Go 的内建函数 append 为什么会返回一个新的 slice?
Go 源码中对于 slice 的声明是这样的:
type slice struct {
array unsafe.Pointer
len int
cap int
}
并且切片还是一个指针,所以可以认为 s := make([]string, 10)
约等于 s := &slice{}
。
那 append
方法既然接收 *slice
作为参数,为什么还会返回一个新的 slice 呢?它完全可以修改当前的 slice 呀,再创建一个 *slice
返回不是会产生更多的内存申请和释放吗?
因为切片会扩容,产生新的slice
首先,官方说的是让你保存返回的 slice,并没有说返回的一定是新的 slice;当没有发生扩容的时候,就是原来的 slice,发生了就不是原来的 slice。。。 至于为何扩容之后就不是原来的 slice,那是因为 slice 结构体包含三个部分,指向数组的指针、长度(len)、容量(cap),当进行 append 操作时,元素就会被追加到 指针指向的数组,在 go 中,数组是固定长度的,一旦超过数组的长度,那么 slice 会扩容,就是重建一个新的大数组,然后将指针指向这个新的数组,这时候,slice 就不是原来的 slice
回答
往 slice 里面添加元素的时候,可以分为两种情况:
如果 len > cap,slice 就要扩容,slice扩容会开辟一个「新的底层数组」,并且将 slice指针 指向这个「新的底层数组」,也会将「旧的底层数组元素」全部放到「新的底层数组」里面,最后在「新的底层数组」里面添加新元素。
之所以不在「旧的底层数组」上进行扩容,可能是因为「旧的底层数组」后面没有连续的一段内存可以进行扩容,无法衔接到「旧的底层数组」上去,所以需要开辟一个「新的底层数组」,并用slice指针指向这个「新的底层数组」,这样返回的自然就是一个新的slice指针了,因为slice的len + 1,cap扩大了,根据slice的源码,可知slice结构体中有指针、len和cap,扩容后,slice里面的指针、len和cap改变了,那slice自然就改变了,自然就是新的slice了。「旧的底层数组」由垃圾回收机制自动回收。
如果 len <= cap,slice 就不扩容,直接在「旧的底层数组」上添加新元素即可,但返回的slice还是新的,因为slice的底层源码里面有len这一变量,添加元素,slice的len是会改变的,所以slice是会改变的,自然返回的就是一个新的slice了。
综上所述,append添加元素到slice中,返回的一定是一个新的slice,如果 len > cap 要进行扩容,就算是开辟一个更大的「新的底层数组」也是必要的。
扩展
1. 两个slice变量共用同一个底层数组,append添加元素到其中一个变量中,两个slice是不是还指向同一个底层数组?
代码示例
输出结果
s2 和 s3 共用一个底层数组,往 s3中append 新元素后,因为不扩容,所以 s3 还是和 s2 共用一个底层数组 (从s2 和 s3 的底层数组的首地址一样可以看出)
2. 扩容后返回的新slice的首地址有没有改变,底层数组的首地址有没有改变
代码示例
输出结果
可以看到「slice的首地址」没有变,但是扩容过后,「slice指向的底层数组的首地址」发生了改变,也就是说slice扩容是其指向的底层数组开辟一个新的内存空间,而不是slice开辟一个新的内存空间。
根据slice源码和上面的示例代码,就算是扩容,slice也只是修改了结构体里面的指针、len和cap而已,并没有开辟一个新的slice内存空间。