slice全解析
昨天組內小夥伴做分享,給出了這麼一段代碼:
package mainimport ( "fmt")func fun1(x int) { x = x + 1}func fun2(x *int) { *x = *x + 1}func fun3(x []int) { x = append(x, 3)}func fun4(x *[]int) { *x = append(*x, 3)}func fun5(x [4]int) { x[3] = 100}func fun6(x *[4]int) { (*x)[3] = 200}// 傳值,傳指標func main() { x1 := 10 fmt.Printf("%+v\n", x1) fun1(x1) fmt.Printf("%+v\n\n", x1) fmt.Printf("%+v\n", x1) fun2(&x1) fmt.Printf("%+v\n\n", x1) var x3 []int x3 = append(x3, 0, 1, 2) fmt.Printf("%+v\n", x3) fun3(x3) fmt.Printf("%+v\n\n", x3) fmt.Printf("%+v\n", x3) fun4(&x3) fmt.Printf("%+v\n\n", x3) var x4 [4]int for i := 0; i < 4; i++ { x4[i] = i } fmt.Printf("%+v\n", x4) fun5(x4) fmt.Printf("%+v\n\n", x4) fmt.Printf("%+v\n", x4) fun6(&x4) fmt.Printf("%+v\n\n", x4)}
可以放在play上運行一下,實際輸出的是
10101011[0 1 2][0 1 2][0 1 2][0 1 2 3][0 1 2 3][0 1 2 3][0 1 2 3][0 1 2 200]
得出的結論是:slice是引用傳遞,數組是值傳遞,但是要想修改slice和數組,都需要把slice或者數組的地址傳遞進去。
這個結論中的數組是值傳遞,要在調用函數內部修改數組值,必須傳遞數組指標,我沒有什麼意見。但是slice的部分,卻並沒有那麼簡單。基本上,需要明確下面幾點才能解釋上面的代碼。
slice的結構是uintptr+len+cap
比如我定義了一個slice, 不管是什麼方法定義的
var a []inta = make([]int, 1)a := []int{1,2}
這裡的a都是由一個固定的資料結構賦值的
這個資料結構有三個,一個是指向一個定長數組的指標,一個是len,表示我這個slice包含了幾個值,還有一個是cap,表示我申請定長數組的時候申請了多大的空間。
slice的append操作是根據cap和len的關係判斷是否申請新的空間
在記憶體看空間中,沒有不定長的數組,所有不定長數組的文法都是語言本身封裝了。比如golang中的slice。slice可以在初始化的時候就定義好我需要使用多大的空間(cap)
a := make([]int, 1, 10)
這裡的10也就是cap,1是len,說明我已經建立了10個int空間給這個slice。
當不斷往a中append資料的時候,首先是len不斷增加,當len和cap一樣的時候,這個時候再append資料,就會新開闢一個數組空間,這個數組空間長度為多大呢?2*cap。
舉例說,如果上述的a後續又append了9個資料,這個時候如果再append一個資料,就會發現cap變成20了。
當然,如果擴容了,那麼我們說的slice的第一個元素,指向定長數組的地址就會變化。
理解下下面這個代碼:
package mainimport "fmt"import "unsafe"func main() { var a []int a = append(a, 0) printSlice("a", a) a = append(a, 1) printSlice("a", a) a = append(a, 2, 3, 4) printSlice("a", a)}func printSlice(s string, x []int) { fmt.Printf("%s len=%d cap=%d ptr=%p %v\n",s, len(x), cap(x), unsafe.Pointer(&x[0]), x)} 輸出:a len=1 cap=2 ptr=0x10414020 [0]a len=2 cap=2 ptr=0x10414020 [0 1]a len=5 cap=8 ptr=0x10458020 [0 1 2 3 4]
函數參數是slice的時候傳遞的是“slice結構”的值拷貝
我們說的slice為參數傳遞的時候傳遞是引用傳遞,實際上,它傳遞的是Slice結構(uintptr+len+cap)的一個複製,但是由於uintptr對應的是一個定長的數組,所以基本上當slice作為參數傳遞的時候,返回回來的slice結構是不會變的,對應的定長數組的大小是不會變的,但是這個定長數組裡面的具體值是有可能變的。
看下面幾個例子:
package mainimport ( "fmt" "unsafe")func fun3(x []int) { fmt.Printf("%p\n", unsafe.Pointer(&x[0])) x = append(x, 3) x[2] = 100 fmt.Printf("%p\n", unsafe.Pointer(&x[0]))}func main() { var x3 []int x3 = append(x3, 0, 1, 2) fmt.Printf("%+v\n", x3) fmt.Printf("%p\n", unsafe.Pointer(&x3[0])) fun3(x3) fmt.Printf("%p\n", unsafe.Pointer(&x3[0])) fmt.Printf("%+v\n\n", x3)}輸出:[0 1 2]0x104140200x104140200x104140200x10414020[0 1 100]
這裡的x3[2]的的值變化了。但是slice的指標地址沒有變化。
如果在f3裡面修改x[3]的值:
package mainimport ( "fmt" "unsafe")func fun3(x []int) { fmt.Printf("%p\n", unsafe.Pointer(&x[0])) x = append(x, 3) x[3] = 100 fmt.Printf("%p\n", unsafe.Pointer(&x[0]))}func main() { var x3 []int x3 = append(x3, 0, 1, 2) fmt.Printf("%+v\n", x3) fmt.Printf("%p\n", unsafe.Pointer(&x3[0])) fun3(x3) fmt.Printf("%p\n", unsafe.Pointer(&x3[0])) fmt.Printf("%+v\n\n", x3)}輸出:[0 1 2]0x104140200x104140200x104140200x10414020[0 1 2]
這裡的x3的值就不會變化,雖然不會變化,但是實際上slice指向的定長數組的索引為3的值已經變化了。
如果f3是append兩個呢?
package mainimport ( "fmt" "unsafe")func fun3(x []int) { fmt.Printf("%p\n", unsafe.Pointer(&x[0])) x = append(x, 3, 4) x[3] = 100 fmt.Printf("%p\n", unsafe.Pointer(&x[0]))}func main() { var x3 []int x3 = append(x3, 0, 1, 2) fmt.Printf("%+v\n", x3) fmt.Printf("%p\n", unsafe.Pointer(&x3[0])) fun3(x3) fmt.Printf("%p\n", unsafe.Pointer(&x3[0])) fmt.Printf("%+v\n\n", x3)}輸出:[0 1 2]0x104140200x104140200x104580000x10414020[0 1 2]
我們看到在fun3裡面append之後的x這個slice指向的地址變化了。但是由於這個x實際上是我們傳遞進去的x3的值拷貝,所以這個x3並沒有被修改。最後輸出的時候還是沒有變化。
總結
基本上記住了這幾個點就明白了slice:
- slice的結構是uintptr+len+cap
- slice的append操作是根據cap和len的關係判斷是否申請新的空間
- 函數參數是slice的時候傳遞的是“slice結構”的值拷貝
參考