golang slice傳參陷阱


golang slice傳參陷阱



起因


package main

func SliceRise(s []int)  {
	s = append(s, 0)
	for i := range s {
		s[i]++
	}
}

func main()  {
	s1 := []int{1, 2}
	s2 := s1
	s2 = append(s2, 3)
	SliceRise(s1)
	SliceRise(s2)
	fmt.Println(s1, s2)
}

//A: [2,3][2,3,4]
//B: [1,2][1,2,3]
//C: [1,2][2,3,4]
//D: [2,3,1][2,3,4,1]


起因是寢室里的大佬在我干大事的時候突然叫我看一道題,就是上面這段程序。於是我憤怒的馬上進行分析。這道題目來源於《Go專家編程》p14。我思考了很久,想不到一個解釋的通的答案。
答案是選C。


后面在研究這道題的時候,翹出了一個忽略的知識點。那就是關於slice在傳參append時的一些陷阱。


slice的傳參


在初學golang的時候,我一直以為slice是引用傳遞而不是值傳遞,其實不然。


我們先來看一下官方對於這個問題的解釋:


In a function call, the function value and arguments are evaluated in
the usual order. After they are evaluated, the parameters of the call
are passed by value to the function and the called function begins
execution. The return parameters of the function are passed by value
back to the caller when the function returns.

譯文:

在函數調用中,函數值和參數按通常的順序計算。求值之后,調用的參數通過值傳遞給函數,被調用的函數開始執行。當函數返回時,函數的返回參數按值返回給調用者。


來源於:
https://golang.org/ref/spec#Calls


也就是說golang中其實是沒有所謂的引用傳遞的,只有值傳遞。那為什么我們在函數中對slice進行修改時,有時候會影響到函數外部的slice呢?


這就要從slice的內存模型說起了,slice的內存模型其實非常簡單,就是一個結構體,里面包含了三個字段。第一個字段是一個指向底層數組的指針,第二個是slice的長度,第三個是底層數組的大小。具體的可以看這里:https://blog.csdn.net/qq_49723651/article/details/121267698?spm=1001.2014.3001.5501

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

在傳遞參數的時候,其實是傳遞了一一個slice結構體,這個時候當然是值傳遞。我們來驗證一下:


package main

import "fmt"

func SliceRise(s []int)  {
	fmt.Printf("%p\n", &s)
	s = append(s, 0)
	for i := range s {
		s[i]++
	}
}

func main()  {
	s1 := []int{1, 2}
	s2 := s1
	s2 = append(s2, 3)
	fmt.Printf("%p\n", &s1)
	SliceRise(s1)
	//SliceRise(s2)
	//fmt.Println(s1, s2)
}

//輸出
//0xc000004078
//0xc000004090

通過計算可以知道slice結構體的大小為24byte,兩個地址之差剛好是24byte。
地址不同,所以兩個結構體不是同一個結構體。

然而結構體中的指針字段卻包含了底層數組的地址,這就使得函數中的slice和函數外的slice都指向了同一個底層數組,這也就是有些時候,改變函數內部的slice也能影響到函數外部的slice的原因。


slice的擴容


有關擴容的詳細規則可以看這篇博客:https://blog.csdn.net/qq_49723651/article/details/121267698?spm=1001.2014.3001.5501

slice在append的時候,如果底層數組的大小(cap)不夠了,就會發生擴容。發生擴容的時候,slice結構體的指針會指向一個新的底層數組,然后把原來數組中的元素拷貝到新數組中,最后添加上append的新元素,就完成了擴容。
所以在這個時候,函數內部slice的改變是不會影響到函數外部slice的。因為此時,兩個結構體中的指針指向的底層數組已經不相同了。


回到開始


然后我們回到最開始的這段代碼:


package main

func SliceRise(s []int)  {
	s = append(s, 0)
	for i := range s {
		s[i]++
	}
}

func main()  {
	s1 := []int{1, 2}
	s2 := s1
	s2 = append(s2, 3)
	SliceRise(s1)
	SliceRise(s2)
	fmt.Println(s1, s2)
}

//A: [2,3][2,3,4]
//B: [1,2][1,2,3]
//C: [1,2][2,3,4]
//D: [2,3,1][2,3,4,1]


選C也就不難解釋了:

  • 首先s1在初始化的時候,分配了一個底層數組,len=2cap=2
  • 將s1賦值給s2,兩者就指向了同一個底層數組;
  • s2發生擴容,因為cap不夠了,這個時候s2指向一個新的底層數組,並且len=3cap=4
  • 然后調用兩次SliceRise函數;
  • s1作為參數進入函數時,發生了擴容,因為cap不夠了,所以新分配了一個底層數組,這個時候,main函數中的s1與SliceRise中的s1已經分道揚鑣了。所以main函數中的s1不會有任何改變;
  • s2作為參數進入函數時,同樣發生了擴容,但是cap還夠,所以不會分配新的底層數組,接下來的所有改變都會影響到main函數中的s2;
  • 所以最終在main函數中,s1輸出[1,2],而s2輸出[2,3,4]。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM