对于Go研发人员来说,对于slice结构中的长度(length)和容量(capacity)经常混淆是很常见的。完全理解这两个概念对有效处理slice的核心操作是至关重要的。例如:对slice的初始化,使用append添加元素,拷贝元素或分隔slice等操作。否则,可能导致使用append操作切片时性能低下,甚至是内存泄露。在Go语言中,slice的底层实现是数组,也就是说,切片的数据实际上是被存储在数组中的。如果后端的数组空间已经满了或是空数组,则slice结构体负责处理数组容量的扩容或缩容逻辑。此外,slice的结构体中共拥有三个字段:
- 一个指针,指向后端的数组,
- 一个length字段,代表该slice中包含的元素个数。
- 一个capacity(容量)字段,代表后端数组能够容纳的元素个数。
我们通过两个例子来演示一下slice的结构。首先,我们使用给定的长度和容量来初始化一个slice:
① 第二个参数3代表长度(length),第三个参数6代表容量(capacity)
如下图所示:
该切片创建了一个能够容纳6个元素(容量)的数组。同时,因为长度length被设置成了3,所以,Go仅仅初始化前3个元素。因为slice的元素是[]int类型,所以前3个元素用int的零值0来初始化。剩余的元素空间只被分配,但没有使用。如果打印这个切片,将会得到如下结果:[0 0 0]。如果我们设置s[1] = 1,那么,该切片的第2个元素将会被更新,但对该slice的长度和容量不会有任何影响。如下图所以:
但是,不允许访问切片长度(length)以外的元素,即使长度以外的内存空间也已经被分配了。例如,s[4] = 0 会引发panic:
那么,我们该如何使用slice中剩余的空间呢?通过内建的append函数:
该操作将会往s切片中添加一个新的元素。该元素使用第一个图中灰色的元素块(即分配了空间但又没被使用的位置)来存储元素2。如下图所以:
这时,slice的长度length从3变成了4,即该slice现在有4个元素。那如果我们再多加入3个元素slice会发生什么?后端的数组空间会不会不足够大了?
如果我们执行这部分代码,我们会注意到该slice依然能满足我们的需求:
因为数组是一个固定长度的结构,只能将元素4给存储进去。当我们想插入元素5时,该数组就已经满了,Go会创建另一个数组,并且空间大小是原来容量的2倍,然后将原数组中的所有元素都拷贝到新数组中去,再在新数组中插入元素5,如下图所示:
现在slice的的指针字段指向了新的数组。那原来的那个数组会怎么样呢?如果没有被引用,将会被GC进行回收。下面,我们来看看对一个slice进行切分的影响:
① 一个长度为3,容量为6的切片② 从索引1到3进行切分
如下图:
首先,s1被初始化成一个长度为3,容量为6的切片。当通过切分s1创建s2切片时,s1和s2的指针字段都指向同一个后端数组。但是,s2的第一个元素的索引是从数组的索引1开始的。因此,切片s2的长度和容量是和s1不同的:长度为2,容量为5.如果我们更新s1[1]或s2[0],那么对于后端数组来说,变更是一样的。因此,该变更对两个切片都是可见的,如图4.6所示:
那,如果现在往s2中append一个元素会发生什么呢?会对s1有影响吗?
这样,会将共享的数组进行修改,但只有s2的长度会发生改变,如图4.7所示:
s1的长度依然是3,容量是6.因此,如果我们打印s1和s2,那么被加入的元素只对s2可见:
在使用append时,理解这个行为会降低出错的概率。
最后一个需要注意的是,如果我们持续往s2中append元素,直到数组满了位置,会发生什么呢?我们再往s2中增加3个元素,直到将后端的数组填满,没有任何可用的空间:
① 在该阶段,后端的数组就已经满了。
这段代码会导致创建另一个新的数组,如图所示:
注意,这时s1和s2分别指向了两个不同的数组。实际上,s1依然是一个长度为3,容量为6的切片,同时也有一些可用的buffer空间,因此,它依然是引用了最初的那个数组。同时,新创建的数组,会从s2的起始位置将数据拷贝到自己的空间上来。这也就是为什么新数组的第一个元素是1,而不是0的原因。总之,切片中的length是该切片中当前已存储的元素个数,切片的容量是该切片指向的数组的元素个数。往一个满了的切片(切片长度=切片容量)中添加新元素会触发创建一个新的数组,并且新数组的容量是原来的2倍,该新数组会将原数组中的元素都拷贝过来,同时将slice中的指针更新到指向新数组。
至此,我还总结了一篇slice的常见问答可参考:Go常见错误集锦之slice数据结构问答