我们看到切片 s 的 len 值是线性增长的,但 cap 值却呈现出不规则的变化。通过下图,我们更容易看清楚多次 append 操作究竟是如何让切片进行动态扩容的。
切片的动态扩容
我们看到 append 会根据切片对底层数组容量的需求对底层数组进行动态调整。
1)最初 s 初值为零值(nil),此时 s 没有绑定底层数组。
2)通过 append 操作向切片 s 添加一个元素 11,此时 append 会首先分配底层数组 u1(数组长度 1),然后将 s 内部表示中的 array 指向 u1,并设置 len = 1,cap =1。
3)通过 append 操作向切片 s 再添加一个元素 12,此时 len (s) = 1,cap (s) = 1,append 判断底层数组剩余空间不满足添加新元素的要求,于是创建了一个新的底层数组 u2,长度为 2(u1 数组长度的 2 倍),并将 u1 中的元素复制到 u2 中,最后将 s 内部表示中的 array 指向 u2,并设置 len = 2,cap = 2。
4)通过 append 操作向切片 s 再添加一个元素 13,此时 len (s) = 2,cap (s) = 2,append 判断底层数组剩余空间不满足添加新元素的要求,于是创建了一个新的底层数组 u3,长度为 4(u2 数组长度的 2 倍),并将 u2 中的元素复制到 u3 中,最后将 s 内部表示中的 array 指向 u3,并设置 len = 3,cap 为 u3 数组长度,即 4。
5)通过 append 操作向切片 s 再添加一个元素 14,此时 len (s) = 3,cap (s) = 4,append 判断底层数组剩余空间满足添加新元素的要求,于是将 14 放在下一个元素的位置(数组 u3 末尾),并将 s 内部表示中的 len 加 1,变为 4。
6)通过 append 操作向切片 s 添加最后一个元素 15,此时 len (s) = 4,cap (s) = 4,append 判断底层数组剩余空间不满足添加新元素的要求,于是创建了一个新的底层数组 u4,长度为 8(u3 数组长度的 2 倍),并将 u3 中的元素复制到 u4 中,最后将 s 内部表示中的 array 指向 u4,并设置 len = 5,cap 为 u4 数组长度,即 8。
我们看到 append 会根据切片的需要,在当前底层数组容量无法满足的情况下,动态分配新的数组,新数组长度会按一定算法扩展(参见 $GOROOT/src/runtime/slice.go 中的 growslice 函数)。新数组建立后,append 会把旧数组中的数据复制到新数组中,之后新数组便成为切片的底层数组,旧数组后续会被垃圾回收掉。这样的 append 操作有时会给 Gopher 带来一些困惑,比如通过语法 u [low: high] 形式进行数组切片化而创建的切片,一旦切片 cap 触碰到数组的上界,再对切片进行 append 操作,切片就会和原数组解除绑定:
我们看到在添加元素 25 之后,切片的元素已经触碰到底层数组 u 的边界;此后再添加元素 26,append 发现底层数组已经无法满足添加新元素的要求,于是新创建了一个底层数组(数组长度为 cap (s) 的 2 倍,即 8),并将原切片的元素复制到新数组中。在这之后,即便再修改切片中的元素值,原数组 u 的元素也没有发生任何改变,因为此时切片 s 与数组 u 已经解除了绑定关系,s 已经不再是数组 u 的描述符了。