分析append引出的切片内存问题
今天群里讨论一个关于切片append的坑
a := []int{1}
a = append(a,2)
a = append(a,3)
b := append(a,4)
c := append(a,5)
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
一般思路我们是想到添加元素后b为[1,2,3,4],c为[1,2,3,5],但是为什么结果会是两个[1,2,3,5],4去哪儿了?其实一般思路是错误的。在理解append函数用法之前,我们应该先了解golang的切片内存模式是什么样的。
将源代码改为输出切片的长度和容量,还有切片引用的地址
a := []int{1}
fmt.Println("cap(a) =", cap(a), "len(a)=", len(a), "ptr(a) =", &a[0])
a = append(a, 2)
fmt.Println("cap(a) =", cap(a), "len(a)=", len(a), "ptr(a) =", &a[0])
a = append(a, 3)
fmt.Println("cap(a) =", cap(a), "len(a)=", len(a), "ptr(a) =", &a[0])
b := append(a, 4)
fmt.Println("cap(a) =", cap(a), "len(a)=", len(a), "len(b)=", len(b), "ptr(a) =", &a[0], "ptr(b) =", &b[0])
c := append(a, 5)
fmt.Println("cap(a) =", cap(a), "len(a)=", len(a), "len(c)=", len(c), "ptr(a) =", &a[0], "ptr(c) =", &c[0])
先来理解以下代码深入理解切片内存,运行观察结果
//创建底层数组,在底层数组基础上创建切片
fmt.Println("------1.创建底层数组,在底层数组基础上创建切片------")
num := [10]int{0,1,2,3,4,5,6,7,8,9}//len=10
s1 := num[:5]
s2 := num[3:8]
s3 := num[5:]
s4 := num[:]
fmt.Println("num:", num)//[0,1,2,3,4,5,6,7,8,9]
fmt.Println("s1:", s1)//[0,1,2,3,4]
fmt.Println("s2:", s2)//[3,4,5,6,7]
fmt.Println("s3:", s3)//[5,6,7,8,9]
fmt.Println("s4:", s4)//[0,1,2,3,4,5,6,7,8,9]
fmt.Printf("%p\n",&num) //0xc000014230
fmt.Printf("%p\n",s1) //0xc000014230,与num的地址相同
fmt.Printf("s1 len:%d ,cap:%d \n",len(s1),cap(s1)) // len:5 ,cap:10
fmt.Printf("s2 len:%d ,cap:%d \n",len(s2),cap(s2)) // len:5 ,cap:7
fmt.Printf("s3 len:%d ,cap:%d \n",len(s3),cap(s3)) // len:5 ,cap:5
fmt.Printf("s4 len:%d ,cap:%d \n",len(s4),cap(s4)) //len:10 ,cap:10
//修改底层数组
fmt.Println("------2.修改底层数组------")
num[4] = 100
fmt.Println(s1) //[0 1 2 3 100]
fmt.Println(s2) //[3 100 5 6 7]
fmt.Println(s3) //[5 6 7 8 9]
fmt.Println(s4) //[0 1 2 3 100 5 6 7 8 9]
//修改切片
fmt.Println("------3.修改切片------")
s2[1] = 99
fmt.Println(num) //[0 1 2 3 99 5 6 7 8 9]
fmt.Println(s1) //[0 1 2 3 99]
fmt.Println(s2) //[3 99 5 6 7]
fmt.Println(s3) //[5 6 7 8 9]
fmt.Println(s4) //[0 1 2 3 99 5 6 7 8 9]
fmt.Println("------4.使用append修改切片内容------")
s1 = append(s1,1,1,1,1)
fmt.Println(num) //[0 1 2 3 99 1 1 1 1 9]
fmt.Println(s1) //[0 1 2 3 99 1 1 1 1]
fmt.Println(s2) //[3 99 1 1 1]
fmt.Println(s3) //[1 1 1 1 9]
fmt.Println(s4) //[0 1 2 3 99 1 1 1 1 9]
fmt.Println(len(s1),cap(s1)) //len=9,cap=10
fmt.Println("-------5.append添加元素扩容-------")
s1 = append(s1,2,2,2,2,2) //因为s1的len=9,cap=10,添加5个元素cap不够,只能扩容重新指向一个新的底层数组
fmt.Println(num) //[0 1 2 3 99 1 1 1 1 9]
fmt.Println(s1) //[0 1 2 3 99 1 1 1 1 2 2 2 2 2]
fmt.Println(s2) //[3 99 1 1 1]
fmt.Println(s3) //[1 1 1 1 9]
fmt.Println(s4) //[0 1 2 3 99 1 1 1 1 9]
fmt.Printf("%p\n",&num) //0xc000014230,地址没变
fmt.Printf("%p\n",s1) //0xc000086000,地址改变,这是因为扩容后重新指向了一个新的底层数组
fmt.Println(len(s1),cap(s1)) //s1的len=14,,cap=20.cap扩容是在原来10的基础上成倍扩容
观察以上代码,不难发现,每一个切片s都引用了同一个底层数组num,即切片本身不存储数据,都是底层数组存储数据,而切片只不过是对底层数组的一段引用,直接修改切片内容的同时,底层数组也会随其改变。往切片添加数据时,若没有超过切片的cap,那么直接添加,并且添加的值会相应覆盖底层数组的原值(如上述代码4步骤中往s1后添加多个数据)
切片是如何内存分配以及扩容的?
s := []int{1,2,3} //len:3,cap:3
s = append(s,4,5) //len:5,cap:6
s = append(s,6,7,8) //len:8,cap:12
s = append(s,9,10) //len:10,cap:12
再修改代码运行结果
a := []int{1}
a = append(a,2)
a = append(a,3)
b :=append(a,4)
fmt.Println(b)
fmt.Println("----分割线-----")
c :=append(a,5)
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
很明显问题出在b和c定义赋新切片的操作
关于切片的append方法,官方给出的解释如下
如何使用append函数?
slice2 := append(slice1,23,15)
以上对切片slice1进行append操作,该操作遵循以下原则:
1.append
函数对一个切片slice1
进行追加操作,并返回另一个长度为len(slice1)+追加个数
的切片,原切片不被改动,两个切片所指向的底层数组可能是同一个也可能不是,取决于第二条:
2.slice1
是对底层数组的一段引用,若append
追加完之后没有突破slice1
的容量,则实际上追加的数据改变了其底层数组对应的值,并且append
函数返回对底层数组的新引用(切片);若append
追加的数量突破了slice1
的最大容量(底层数组长度固定,无法增加长度赋予新值),则Go
会在内存中申请新的数组(数组内的值为追加操作之后的值),并返回对新数组的引用(切片)
(人话来说就是先判断切片容量,没满则修改,满就扩容成新的返回新的切片)
切片共享内部结构
a := []int{1,2}
//fmt.Println(cap(a))//2
b := append(a[0:1],3)
fmt.Println(a[1:2])//[3]
c := append(a[1:2],4)
//fmt.Println(cap(c))//2
fmt.Println(a)//[1,3]
fmt.Printf("a的地址是: %p \n",&a[0])//0xc0000a0090
fmt.Println(b)//[1,3]//切片容量没满,可修改内容
fmt.Printf("b的地址是: %p \n",&b[0])//0xc0000a0090
fmt.Println(c)//[3,4]//切片容量已满,新切片
fmt.Printf("c的地址是: %p \n",&c[0])//0xc0000a00c0