從一道題來看看golang中的slice作為引數時的現象

2023-11-16 06:00:14

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 內建函數