Go中的切片append相关细节: 你真的懂了吗

59 阅读4分钟

1. 首先思考一个问题:如何计算slice[i : j]和slice[i : j : k]长度和容量

对底层数组容量是k的切片slice[i:j]来说

长度 : j - i 容量 : k - i

示例

//创建一个整形切片
//其长度和容量都是5个元素
slice := []int{10, 20, 30, 40, 50}

//创建一个新切片
//其长度为2个元素,容量为4个元素
newSlice := slice[1:3]

对newSlice来说, 长度 : 3 - 1 = 2 容量 : 5 - 1 = 4

需要记住的是,现在两个切片共享一个底层数组。如果一个切片修改了该底层数组的共享部分,另一个切片也会改变。

对于slice[i : j : k]来说

对于slice[i : j : k]或[2 : 3 : 4]来说 长度 : j - i 或 3 - 2 = 1 容量 : k - i 或 4 - 2 = 2

第一个值表示新切片开始的元素的索引位置,这个例子中是2. 第二个值表示开始的索引位置加上希望包括的元素的个数(1),2+1的结果是3,所以第三个值就是3.(只包含一个索引为2的元素) 为了设置容量,从索引2的位置开始,加上希望容量中包含的元素的个数(2),就得到了第三个值4

2. 切片的append底层原理

append调用会检查slice是否有足够的容量存储新的元素。

  1. 如果容量足够,它会定义一个新的slice(但是仍然引用原始底层数组),然后将新的元素复制到新的位置,并返回这个新的slice。

示例

d2cfcfe752aa4a70a0866f20b5a2e873.png

因为newSlice在底层数组还有额外的容量可用,append操作将可用的元素合并到切片的长度,并对其赋值。由于和原始的slice共用一个底层数组,slice中索引为3的元素的值也被改动了。



  1. 如果切片的底层数组没有足够的可用容量,append函数会创建一个拥有足够容量的新的底层数组,将元素从slice x复制到新数组里,再追加新的值。

即:如果slice原来的容量够添加这些元素,append生成的新slice底层数组还是原来的(地址是同一个)然后再在后面添加新元素 如果不够,则会创建一个新的底层数组(地址不一样)进行拷贝和添加**

append会智能地处理底层数组的容量增长-扩容机制

在切片容量小于1000个元素时,总是会成倍地增加容量。(如长度为4,容量为4的切片在append之后变为长度为5,容量为8)

一旦元素的个数超过1000,容量的增长因子会设为1.25,也就是每次增加25%。

随着语言的演化,这种增长算法可能会有所改变。

3. 如何安全使用append

问题

我们之前讨论过,内置函数append会首先使用可用容量。一旦没有可用容量,会分配一个新的底层数组。这导致很容易忘记切片间正在共享同一个底层数组。一旦发生这种情况,对切片进行修改,很可能会导致随机而且奇怪的问题。对切片内容的修改会影响多个切片,却很难找到问题的原因

解决方法

如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个append操作创建新的底层数组,与原有的底层数组分离

新切片与原有的底层数组分离后,可以安全的进行后续修改。

示例

source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}

//对第三个元素做切片,并限制容量
//其长度和容量都是1
slice := source[2:3:3]

//向slice追加新字符串
slice = append(slice, "Kiwi")

如果不加第三个索引,由于剩余的所有容量都属于slice,向slice追加Kiwi会改变原底层数组索引为3的Banana。

不过在该代码中我们限制了slice的容量为1 当我们第一次对slice调用append的时候,会创建一个新的底层数组

这个数组包含两个元素,并将水果Plum复制进来,再追加新的Kiwi,并返回一个引用了这个底层数组的新切片

本文章内容源自<Go语言实战>

大佬的示例讲解

Go Slice 扩容的这些坑你踩过吗? - 掘金 (juejin.cn)