Go常见错误集锦之混淆slice中的长度(length)和容量(capacity)

发布时间 2023-06-25 13:42:51作者: 图兜

原文文章:https://zhuanlan.zhihu.com/p/413972333

 

在Go语言中,slice的底层实现是数组,也就是说,切片的数据实际上是被存储在数组中的。如果后端的数组空间已经满了或是空数组,则slice结构体负责处理数组容量的扩容或缩容逻辑。

此外,slice的结构体中共拥有三个字段:

  • 一个指针,指向后端的数组,
  • 一个length字段,代表该slice中包含的元素个数。
  • 一个capacity(容量)字段,代表后端数组能够容纳的元素个数。

我们通过两个例子来演示一下slice的结构。

首先,我们使用给定的长度和容量来初始化一个slice:

s := make([]int, 3, 6) ①

① 第二个参数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:

panic:runtime error: index out of range [4] with length 3

那么,我们该如何使用slice中剩余的空间呢?通过内建的append函数:

s = append(s, 2)

该操作将会网s切片中添加一个新的元素。该元素使用第一个图中灰色的元素块(即分配了空间但又没被使用的位置)来存储元素2。如下图所以:

这时,slice的长度length从3变成了4,即该slice现在有4个元素。

那如果我们再多加入3个元素slice会发生什么?后端的数组空间会不会不足够大了?

s = append(s, 3)
s = append(s, 4)
s = append(s, 5)
fmt.Println(s)

如果我们执行这部分代码,我们会注意到该slice依然能满足我们的需求:

[0 1 0 2 3 4 5]

因为数组是一个固定长度的结构,只能将元素4给存储进去。当我们想插入元素5时,该数组就已经满了,Go会创建另一个数组,并且空间大小是原来容量的2倍,然后将原数组中的所有元素都拷贝到新数组中去,再在新数组中插入元素5,如下图所示:

现在slice的的指针字段指向了新的数组。那原来的那个数组会怎么样呢?如果没有被引用,将会被GC进行回收。

下面,我们来看看对一个slice进行切分的影响:

s1 := make([]int, 3, 6) ①
s2 := s1[1:3] ②

① 一个长度为3,容量为6的切片 ② 从索引1到3进行切分

如下图:

首先,s1被初始化成一个长度为3,容量为6的切片。当通过切分s1创建s2切片时,s1和s2的指针字段都指向同一个后端数组。但是,s2的第一个元素的索引是从数组的索引1开始的。因此,切片s2的长度和容量是和s1不同的:长度为2,容量为5.

如果我们更新s1[1]或s2[0],那么对于后端数组来说,变更是一样的。因此,该变更对两个切片都是可见的,如图所示:

那,如果现在往s2中append一个元素会发生什么呢?会对s1有影响吗?

s2 = append(s2, 2)

这样,会将共享的数组进行修改,但只有s2的长度会发生改变,如图所示:

s1的长度依然是3,容量是6.因此,如果我们打印s1和s2,那么被加入的元素只对s2可见:

s1 = [0 1 0], s2 = [1 0 2]

在使用append时,理解这个行为会降低出错的概率。

最后一个需要注意的是,如果我们持续往s2中append元素,直到数组满了位置,会发生什么呢? 我们再往s2中增加3个元素,直到将后端的数组填满,没有任何可用的空间:

s2 = append(s2, 3)
s2 = append(s2, 4)
s2 = append(s2, 5) ①

① 在该阶段,后端的数组就已经满了。

这段代码会导致创建另一个新的数组,如图所示:

注意,这时s1和s2分别指向了两个不同的数组。实际上,s1依然是一个长度为3,容量为6的切片,同时也有一些可用的buffer空间,因此,它依然是引用了最初的那个数组。同时,新创建的数组,会从s2的起始位置将数据拷贝到自己的空间上来。这也就是为什么新数组的第一个元素是1,而不是0的原因。

总之,切片中的length是该切片中当前已存储的元素个数,切片的容量是该切片指向的数组的元素个数。往一个满了的切片(切片长度=切片容量)中添加新元素会触发创建一个新的数组,并且新数组的容量是原来的2倍,该新数组会将原数组中的元素都拷贝过来,同时将slice中的指针更新到指向新数组。

发布于 2021-09-25 2