分析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