Go 的内建函数 append 为什么会返回一个新的 slice? append添加元素到slice中,slice扩容后的细节

一、slice的源码

type slice struct {
    array unsafe.Pointer		// slice 所指向的底层数组
    len   int							// slice 的长度
    cap   int							// slice 的容量
}

二、回答:Go 的内建函数 append 为什么会返回一个新的 slice?

往 slice 里面添加元素的时候,可以分为两种情况:

  1. ① 如果 len > cap,slice 就要扩容,slice扩容会开辟一个「新的底层数组」,并且将 slice指针 指向这个「新的底层数组」,也会将「旧的底层数组元素」全部放到「新的底层数组」里面,最后在「新的底层数组」里面添加新元素。

    之所以不在「旧的底层数组」上进行扩容,可能是因为「旧的底层数组」后面没有连续的一段内存可以进行扩容,无法衔接到「旧的底层数组」上去,所以需要开辟一个「新的底层数组」,并用slice指针指向这个「新的底层数组」,这样返回的自然就是一个新的slice指针了,因为slice的len + 1,cap扩大了,根据slice的源码,可知slice结构体中有指针、len和cap,扩容后,slice里面的指针、len和cap改变了,那slice自然就改变了,自然就是新的slice了。「旧的底层数组」由垃圾回收机制自动回收。

  2. ② 如果 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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值