1、題目
最近看群友在群裡問一道關於golang中slice的題,題目如下:
package main
import "fmt"
func main() {
k := []int{1, 2, 3, 4}
k = append(k, 5, 6)
fmt.Printf("k --> value: %v, add: %p, cap: %d\n", k, k, cap(k))
ap(k)
fmt.Printf("k --> value: %v, add: %p, cap: %d\n", k, k, cap(k))
}
func ap(k []int) {
k = append(k, 7, 8)
fmt.Printf("k --> value: %v, add: %p, cap: %d\n", k, k, cap(k))
}
執行結果:
k --> value: [1 2 3 4 5 6], add: 0xc00001e180, cap: 8
k --> value: [1 2 3 4 5 6 7 8], add: 0xc00001e180, cap: 8
k --> value: [1 2 3 4 5 6], add: 0xc00001e180, cap: 8
乍一看,還挺奇怪的,變數k
的地址都是一樣的,為啥會執行ap
函式時,列印出來的東西不一樣呢?
其實對於初次接觸 golang 的 gopher 而言,這個問題確實有點奇怪,書上不是說slice是引用型別,golang 中的函式傳參是值複製
,那麼在函式傳遞 slice 時,傳遞也是地址,為啥對地址指向的內容做了修改後,並沒有影響到其他指向同一地址的變數呢?
想要理解這裡面的原理,需要了解下面的基礎知識,接下來我們先看看前置知識,學習完這些前置的理論後,相信大家都已經有了自己的理解與答案。
PS: 要是有理解不對的地方,請不吝賜教哈,謝謝。
2、前置理論
2.1、切片的本質
下面的介紹基於 go 1.18,golang中關於 slice 封裝的原始碼位於
runtime/slice.go
中。
切片的本質就是對底層陣列的封裝,切片實際上是一個 struct ,包含了三個欄位:底層陣列的指標、切片的長度(len)和切片的容量(cap)
。
type slice struct {
array unsafe.Pointer // 陣列指標
len int // 長度
cap int // 容量
}
slice 作為引數傳遞的時候,是將slice struct中的各個欄位逐一複製到新的變數中去的,其中 array 欄位是底層陣列的首地址
。
我們一起來看看題目中變數K的初始化
k := []int{1, 2, 3, 4}
k = append(k, 5, 6)
變數 K 示意圖:
執行 ap
函式後
func ap(k []int) {
k = append(k, 7, 8) // 無需擴容,容量足夠
fmt.Printf("k --> value: %v, add: %p, cap: %d\n", k, k, cap(k))
}
函式內變數k的示意圖:
2.2、格式化字串%p列印slice時顯示的是什麼
這個問題呢,推薦大家看下這篇文章,比我說得清楚寫。
[golang slice切片到底是指標嗎?為什麼%p輸出的切片是地址?](https://segmentfault.com/a/1190000042430248)
這裡我們寫一個demo驗證下
func main() {
k := []int{1, 2, 3, 4}
fmt.Printf("k --> add: %p\n", k)
fmt.Printf("k[0] --> add: %p\n", &k[0])
}
執行結果:
k --> add: 0xc000136000
k[0] --> add: 0xc000136000
3、再看題目
瞭解了上面的知識後,再看開頭的題目就很簡單了,變數k 傳給 ap 函式函式時,雖然函式 ap 的形參也叫 k,但是已經不是同一個變數了,只是兩個 slice 指向的底層陣列是同一個而已,所以使用 %p
列印時,顯示的地址是一樣的。
package main
import "fmt"
func main() {
k := []int{1, 2, 3, 4}
k = append(k, 5, 6)
fmt.Printf("k --> value: %v, add: %p, len: %d, cap: %d\n", k, k, len(k), cap(k))
fmt.Printf("k --> add: %p\n", &k)
ap(k)
fmt.Printf("k --> value: %v, add: %p, len: %d, cap: %d\n", k, k, len(k), cap(k))
fmt.Printf("k --> add: %p\n", &k)
}
func ap(k []int) {
k = append(k, 7, 8)
fmt.Printf("k --> value: %v, add: %p, len: %d, cap: %d\n", k, k, len(k), cap(k))
fmt.Printf("k --> add: %p\n", &k)
}
執行結果:
k --> value: [1 2 3 4 5 6], add: 0xc00001e180, len: 6, cap: 8
k --> add: 0xc00000c030
k --> value: [1 2 3 4 5 6 7 8], add: 0xc00001e180, len: 8, cap: 8
k --> add: 0xc00000c078
k --> value: [1 2 3 4 5 6], add: 0xc00001e180, len: 6, cap: 8
k --> add: 0xc00000c030
想要 ap 函式執行後的結果,能夠改變外面的變數k也很簡單,將函式中的形參k返回出去就可以了。類似這樣:
func ap(k []int) []int {
k = append(k, 7, 8)
return k
}
k = ap(k)
是不是有點像 append 內建函式