深入理解 Go 语言——动态数组Slice

欢迎回到我们的公众号,今天,我们将带领你深入理解 Go 语言中的一个重要数据结构:Slice(动态数组)。

Slice 是 Go 语言中非常重要的数据类型,对于我们编写高效且易读的代码至关重要,让我们一起探索这个强大的工具。

什么是 Slice?

Slice 又称动态数组,依托底层数组实现。在 Go 语言中,Slice 是一个引用类型,它比数组更加灵活,更适合处理序列数据。它不仅可以动态地改变自己的大小,还可以进行方便的切片操作。Slice 的定义非常简洁,只有三个字段:指针、长度和容量。

type slice struct {
    array unsafe.Pointer // 数组指针
    len   int 			 // 长度
    cap   int			 // 容量
}
  • array 是一个指向底层数组的指针。
  • len 表示 Slice 当前的长度。
  • cap 表示 Slice 的容量,也就是底层数组的长度。

Slice 的创建和初始化

Go 语言提供了多种创建和初始化 Slice 的方法,这些方法都很简洁且易于使用。

  1. 使用内置的 make 函数创建一个具有指定长度和容量的空 Slice:
var s1[]int // 变量声明

s2 := make([]int, 5, 10) // 创建一个长度为 5,容量为 10 的 int 类型的 Slice
  1. 使用字面量创建和初始化 Slice:
s1:=[]string{} // 空切片

s2 := []int{1, 2, 3, 4, 5} // 创建并初始化一个 int 类型的 Slice
  1. 切取初始化,切片表达式 [low:hight] 表示左闭右开区间,切取的长度是:hight - low
arr := [5]{1,2,3,4,5}

s1 := arr[0:2]  // 从数组中切取:[1,2]
s2 := arr[0:1]  // 从数组中切取:[1]

Slice 的简单操作

Slice 提供了一系列强大的操作,使我们能够方便地处理序列数据。

  • 切片:我们可以使用 [:] 语法方便地对 Slice 进行切片操作,从而获取 Slice 的子序列。
s := []int{1, 2, 3, 4, 5}
sub := s[1:3] // sub 现在是 [2, 3]
  • 追加元素:append() 函数可以将元素追加到 Slice 的末尾。如果 Slice 的容量不足以容纳更多的元素,append 函数会创建一个新的底层数组,并将所有元素复制到新数组中。
s := []int{1, 2, 3}
s = append(s, 4, 5) // s 现在是 [1, 2, 3, 4, 5]
  • 复制元素:copy() 函数可以复制一个 Slice 的元素到另一个 Slice 中。
s1 := []int{1, 2, 3, 4}
s2 := make([]int, 3)
copy(s2, s1) // s2 现在是 [1, 2, 3]

slice copy 不会产生扩容

Slice 实现原理

数据结构

源码包中 src/runtime/slice.go: slice 定义了 slice 的数据结构

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

如果使用 make() 创建 slice 时,可以同时指定长度和容量,创建时底层会分配一个数组,数组的长度即为容量。例如:slice := make(int,5,10) ,该长度长度时 5,容量是 10,可以使用 slice[0] ~ slice[4] 来操作里面的元素。
image.png

slice 扩容

append() 函数可以将元素追加到 Slice 的末尾。如果 Slice 的容量不足以容纳更多的元素,append 函数会创建一个新的底层数组,并将所有元素复制到新数组中,然后返回新 slice ,扩容后再将数据追加进去。实现步骤如下:

  1. 假如 slice 容量够用,则将新元素追加进去,slice.len ++,返回原 slice。
  2. 如果原 slice 不够用,则将 slice 先扩容,扩容后得到新slice。
  3. 将新元素追加进新 slice ,slice.len++,返回新的 slice。

image.png

扩容容量的选择遵循以下规则:

  • 如果原 slice 容量 < 1024,则新的 slice 容量将扩大为原来的 2 倍。
  • 如果原 slice 容量 >= 1024,则新的 slice 将扩容为原来的 1.25 倍。
  • 如果扩容后的大小仍不能满足,那么新 Slice 容量等于所需的容量。
  • 在以上计算完新 Slice 容量后,交由管理内存的组件申请内存,按照给出的表向上取整进行内存申请,申请出来的内存长度,作为Slice扩容后的容量。

扩容函数如下:

func growslice(et *_type, old slice, cap int) slice {
    // ……
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {  //扩容规则就在这里
        newcap = cap
    } else {
        if old.len < 1024 { 
            newcap = doublecap
        } else {
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }
    // ……
    // 如下两行做了内存对齐的操作,是经常导致结果不符合预期的原因,请记住这里
    capmem = roundupsize(uintptr(newcap) * ptrSize)
    newcap = int(capmem / ptrSize)
}

roundupsize 函数如下:
可以看到有3种分支进行不同的内存对齐,在内存对齐的过程中,向上取整,这里涉及到go的内存分配方式。

  var overflow bool
  var lenmem, newlenmem, capmem uintptr
  switch {
  case et.size == 1:
    lenmem = uintptr(old.len)
    newlenmem = uintptr(cap)
    capmem = roundupsize(uintptr(newcap))
    overflow = uintptr(newcap) > maxAlloc
    newcap = int(capmem)
  case et.size == sys.PtrSize:
    lenmem = uintptr(old.len) * sys.PtrSize
    newlenmem = uintptr(cap) * sys.PtrSize
    capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
    overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
    newcap = int(capmem / sys.PtrSize)
  case isPowerOfTwo(et.size):
    ...
  default:
    ...
  }

有一张规律表如下,第二列即是分配的内存大小。

// class  bytes/obj  bytes/span  objects  waste bytes
//     1          8        8192     1024            0
//     2         16        8192      512            0
//     3         32        8192      256            0
//     4         48        8192      170           32
//     5         64        8192      128            0
//     6         80        8192      102           32
//     7         96        8192       85           32
//     8        112        8192       73           16
//     9        128        8192       64            0
//    10        144        8192       56          128
//    11        160        8192       51           32
//    12        176        8192       46           96
//    13        192        8192       42          128
//    14        208        8192       39           80
//    15        224        8192       36          128
//    16        240        8192       34           32
//    17        256        8192       32            0
//    18        288        8192       28          128
//    19        320        8192       25          192
//    20        352        8192       23           96
//    21        384        8192       21          128
//    22        416        8192       19          288
//    23        448        8192       18          128
//    24        480        8192       17           32
//    25        512        8192       16            0
//    26        576        8192       14          128
//    27        640        8192       12          512
//    28        704        8192       11          448
//    29        768        8192       10          512
//    30        896        8192        9          128
//    31       1024        8192        8            0
//    32       1152        8192        7          128
//    33       1280        8192        6          512
//    34       1408       16384       11          896
//    35       1536        8192        5          512
//    36       1792       16384        9          256
//    37       2048        8192        4            0
//    38       2304       16384        7          256
//    39       2688        8192        3          128
//    40       3072       24576        8            0
//    41       3200       16384        5          384
//    42       3456       24576        7          384
//    43       4096        8192        2            0
//    44       4864       24576        5          256
//    45       5376       16384        3          256
//    46       6144       24576        4            0
//    47       6528       32768        5          128
//    48       6784       40960        6          256
//    49       6912       49152        7          768
//    50       8192        8192        1            0
//    51       9472       57344        6          512
//    52       9728       49152        5          512
//    53      10240       40960        4            0
//    54      10880       32768        3          128
//    55      12288       24576        2            0
//    56      13568       40960        3          256
//    57      14336       57344        4            0
//    58      16384       16384        1            0
//    59      18432       73728        4            0
//    60      19072       57344        3          128
//    61      20480       40960        2            0
//    62      21760       65536        3          256
//    63      24576       24576        1            0
//    64      27264       81920        3          128
//    65      28672       57344        2            0
//    66      32768       32768        1            0
Slice 复制 Copy

使用 copy() 内置函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取两个切片长度的最小值,并且拷贝过程中不会发生扩容。
例如:长度为 20 的切片拷贝到长度为 5 的切片中时,将拷贝 5 个元素。

Slice 的内存管理

Slice 的内存管理是通过底层的数组完成的。当我们对 Slice 进行扩容操作时(例如,使用 append 函数添加元素),Go 语言会自动为我们分配一个新的底层数组,并将原有的元素复制到新数组中。这个过程是透明的,我们无需关心底层的细节。
但是,这个特性也意味着我们需要注意 Slice 的内存使用。如果我们长时间保持对一个很大的 Slice 的引用,那么底层数组的内存就不能被回收,可能会导致内存泄漏。因此,我们应该尽量减小 Slice 的生命周期,或者在不再需要大 Slice 时,将其长度设为 0,从而释放内存。

s := make([]int, 1e6) // 创建一个很大的 Slice
s = s[:0] // 当我们不再需要这个大 Slice 时,将其长度设为 0,从而释放内存

总结

Go 语言的 Slice 是一个非常强大的工具,它提供了灵活的动态数组,可以方便地处理序列数据。掌握 Slice 的使用和内部机制,对于我们编写高效且易读的 Go 代码至关重要。
我们在这篇文章中介绍了 Slice 的基本结构、创建和初始化方法、基本操作、实现原理以及内存管理。希望这篇文章能帮助你更深入地理解 Go 语言的 Slice。

  • 每一个切片都指向了一个底层数组。
  • 每一个切片都保存了当前切片的长度、底层数组的可用容量。
  • 使用 len() 计算切片长度的时间复杂度时 O(1),不需要遍历切片。
  • 使用 cap() 计算切片容量的时间复杂度时 O(1),不需要遍历切片。
  • 通过函数传递切片时,不会拷贝整个切片,因为切片本身只是一个结构体而已。
  • 使用 append() 向切片追加元素时有可能触发扩容,扩容后会生成新的切片。
  • 创建切片时最好根据实际需要预分配容量,尽量避免在追加过程中的扩容操作,有利于提升性能。
  • 切片拷贝时需要判断实际拷贝的元素个数
  • 谨慎使用多个切片操作同一个数组,以防读写冲突。

如果你有任何问题或想要讨论的话题,欢迎在评论区留言,我们将尽快回复。在下一篇文章中,我们将继续探讨 Go 语言的更多知识,敬请期待!
感谢你的阅读,如果你觉得这篇文章对你有帮助,欢迎点赞和分享!

请关注公众号【Java千练】,更多干货文章等你来看!
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值