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 返回不是会产生更多的内存申请和释放吗?

讨论数量: 3

因为切片会扩容,产生新的slice

1年前 评论

首先,官方说的是让你保存返回的 slice,并没有说返回的一定是新的 slice;当没有发生扩容的时候,就是原来的 slice,发生了就不是原来的 slice。。。 至于为何扩容之后就不是原来的 slice,那是因为 slice 结构体包含三个部分,指向数组的指针、长度(len)、容量(cap),当进行 append 操作时,元素就会被追加到 指针指向的数组,在 go 中,数组是固定长度的,一旦超过数组的长度,那么 slice 会扩容,就是重建一个新的大数组,然后将指针指向这个新的数组,这时候,slice 就不是原来的 slice

1年前 评论

回答

往 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是不是还指向同一个底层数组?

代码示例

func main() {
    // 两个slice变量是不是指向同一个底层数组,分为两种情况讨论
    // ① slice 扩容的话,就是开辟一个新的slice,并指向新的slice
    s1 := []int{1, 2}
    s2 := s1
    s2[0] = 9          // s2[0]改变,s1[0]也跟着改变,所以此时 s2 和 s1 指向同一个底层数组
    s2 = append(s2, 3) // append导致原本容量为2的s1扩容,改变了底层的数组,也就是重新开了一个数组
    s2[1] = 7          // s2[1]改变,s1[1]没有改变,说明append扩容后,s2没有和s3共用一个底层数组
    fmt.Println("s1=", s1, "s2=", s2)
    fmt.Println("len(s1)=", len(s1), " cap(s1)=", cap(s1))
    fmt.Println("len(s2)=", len(s2), " cap(s2)=", cap(s2))
    fmt.Printf("s1底层数组的首地址:%p\n", &s1[0])
    fmt.Printf("s2底层数组的首地址:%p\n", &s2[0])
    fmt.Println("")


    // ② slice不扩容的话,就是指向同一个底层数组
    s3 := append(s2, 4)
    fmt.Println("s2=", s2, "s3=", s3)
    fmt.Println("len(s2)=", len(s2), " cap(s2)=", cap(s2))
    fmt.Println("len(s3)=", len(s3), " cap(s3)=", cap(s3))
    fmt.Printf("s2底层数组的首地址:%p\n", &s2[0])
    fmt.Printf("s3底层数组的首地址:%p\n", &s3[0])
}

输出结果

s1= [9 2] s2= [9 7 3]
len(s1)= 2  cap(s1)= 2
len(s2)= 3  cap(s2)= 4
s1底层数组的首地址:0xc0000240b0
s2底层数组的首地址:0xc00002a060

s2= [9 7 3] s3= [9 7 3 4]
len(s2)= 3  cap(s2)= 4
len(s3)= 4  cap(s3)= 4
s2底层数组的首地址:0xc00002a060
s3底层数组的首地址:0xc00002a060
  • s1 和 s2 共用一个底层数组,往 s2 中append 新元素后,因为扩容,所以 s2 会指向一个扩容后的底层数组 (从s1 和 s2 的底层数组的首地址不一样可以看出)
  • s2 和 s3 共用一个底层数组,往 s3中append 新元素后,因为不扩容,所以 s3 还是和 s2 共用一个底层数组 (从s2 和 s3 的底层数组的首地址一样可以看出)

2. 扩容后返回的新slice的首地址有没有改变,底层数组的首地址有没有改变

代码示例

func main() {
    s1 := []int{1, 2}
    fmt.Println("原来:len(cap)=", len(s1), "cap(s1)=", cap(s1))
    fmt.Printf("原来s1的地址为:%p\n", &s1)
    fmt.Printf("原来s1所指向的底层数组的首地址:%p\n", &s1[0])
    fmt.Println("")

    s1 = append(s1, 3)
    fmt.Println("append过后,len(cap)=", len(s1), "cap(s1)=", cap(s1))
    fmt.Printf("append过后,s1的地址为:%p\n", &s1)
    fmt.Printf("append过后,s1所指向的底层数组的首地址:%p", &s1[0])
    // 在Go语言中,输出地址后面会有一个%符号,是因为输出的地址是一个指针类型的值。
}

输出结果

原来:len(cap)= 2 cap(s1)= 2
原来s1的地址为:0xc0000a0030
原来s1所指向的底层数组的首地址:0xc0000ac010

append过后,len(cap)= 3 cap(s1)= 4
append过后,s1的地址为:0xc0000a0030
append过后,s1所指向的底层数组的首地址:0xc0000ba000%  

可以看到「slice的首地址」没有变,但是扩容过后,「slice指向的底层数组的首地址」发生了改变,也就是说slice扩容是其指向的底层数组开辟一个新的内存空间,而不是slice开辟一个新的内存空间

根据slice源码和上面的示例代码,就算是扩容,slice也只是修改了结构体里面的指针、len和cap而已,并没有开辟一个新的slice内存空间。

11个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!