目录
一、slice的源码
type slice struct {
array unsafe.Pointer // slice 所指向的底层数组
len int // slice 的长度
cap int // slice 的容量
}
二、回答:Go 的内建函数 append 为什么会返回一个新的 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 要进行扩容,就算是开辟一个更大的「新的底层数组」也是必要的。
三、扩展:append添加元素到slice中,slice扩容后的细节
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内存空间。
四、本文思路
来源于learnku.com里面go社区的提问,大家觉得写的还可以麻烦点个赞👍🏻,如果有什么问题也欢迎在评论区留言与我交流
在learnku.com那边的回答:/https://learnku.com/go/t/75560#reply266602