Go語言slice的本質-SliceHeader

飛雪無情發表於2018-12-31

今天最熱的事情,莫過於微信7.0的釋出,增加了短視訊,優化了看一看等功能,本來想跟著個熱度,蹭個流量,後來發現各位大佬都已經開始蹭了,就算了,還是談談Go語言(golang)吧,看來要成為一個合格的自媒體,還是不要矜持,任重道遠啊。

前兩天有朋友(Weelin)在我的公眾號上留言,留言的文章是這一篇 Go語言實戰筆記(五)| Go 切片 ,這是一篇講Go語言(golang) Slice(切片)的,很早的一篇文章。這位朋友的留言不是講自己的問題,而是針對另外一位朋友(Dreamerque)的留言的說明。

留言起因

為了連貫說明問題,我們先來看下2018-03-17,Dreamerque這位朋友的留言:

有個問題困擾: 考慮將slice這種引用型別作為自定義接受者,並繫結方法如下,

問題: 此時的slice空間容量足夠,呼叫方法前後其地址並不會改變,那麼為何append後的切片內部成員不會改變? 預設拷貝的副本是slice引用,應該要能修改或者新增成員才符合預期的。。

type Slice []int

func (A Slice)Append(value int) {
	A = append(A, value)
}

func main() {
	mSlice := make(Slice, 10, 20)
	mSlice.Append(5)
	fmt.Println(mSlice)
}
複製程式碼

通過程式碼,相信大家也看明白了,以上就是Dreamerque的問題和困惑。我當時給Dreamerque的回答是引用的資料來源不一致,讓他參考我的 Go語言中new和make的區別 這篇文章 。

然後就在前兩天,我收到了Weelin的留言:

無情你好,我理解mslice的資料來源應該是沒發生變化的。由於值拷貝的原因,Append方法前後的切片唯一有關聯的就是底層指向的陣列,列印結果不一樣就是因為原來切片太短了。這個也可以在執行完Append方法後,生成一個新的切片(長度大於5)並列印驗證。

Weelin的留言更細,分析的更準,這時候,我才知道,原來我那個回答,有點誤導Dreamerque了,可能會把我說的資料來源理解成更底層的Data陣列了。

問題分析

從以上的輸出列印中,我們的確可以看到mSlice並沒有任何變化,就是方法Append沒有起任何作用。Dreamerque的困惑是覺得Slice是引用型別,修改了指向應該也會跟著改,其實我們知道,這個修改引用的指向是在Append方法內的,離開就不起作用了。

其實以上都不是根本,根本是Weelin提到的,append後的Slice已經不是原來的Slice了。這時候有的朋友可能又疑惑了,append返回的Slice的指標和原Slice的指標一樣的啊,怎麼會不是一個呢?我們來測試一次,修改程式碼如下:

func (A Slice)Append(value int) {
	A1 := append(A, value)
	fmt.Printf("%p\n%p\n",A,A1)
}
複製程式碼

我們用A1儲存append方法返回的Slice,然後列印返回A1和原A的指標地址,發現的確一樣。大家可以自己執行試試。其實我們自己在make一個Slice的時候會發現,是可以有三個引數的,一個是資料、一個是長度、一個是容量,也就是說,Slice是這樣的一個結構,現在該是我們的SliceHeader登場的時候了。

SliceHeader登場

SliceHeader是Slice執行時的具體表現,它的結構定義如下:

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}
複製程式碼

正好對應Slice的三要素,Data指向具體的底層資料來源陣列,Len代表長度,Cap代表容量。

既然Slice就是SliceHeader,那麼我們把Slice轉化為SliceHeader,來看看AA1內部具體的欄位值,這樣來判斷他們是否一致,我們修改Append方法如下:

//blog:www.flysnow.org
//wechat:flysnow_org

func (A Slice)Append(value int) {
	A1 := append(A, value)

	sh:=(*reflect.SliceHeader)(unsafe.Pointer(&A))
	fmt.Printf("A Data:%d,Len:%d,Cap:%d\n",sh.Data,sh.Len,sh.Cap)

	sh1:=(*reflect.SliceHeader)(unsafe.Pointer(&A1))
	fmt.Printf("A1 Data:%d,Len:%d,Cap:%d\n",sh1.Data,sh1.Len,sh1.Cap)
}
複製程式碼

通過unsafe.Pointer指標進行強制型別轉換,關於unsafe.Pointer的知識可以參考我的 Go語言實戰筆記(二十七)| Go unsafe Pointer 這篇文章。

都轉換為*reflect.SliceHeader型別後,我們分別輸出他們的DataLenCap欄位,現在我們看看輸出的結果。

A  Data:824634204160,Len:10,Cap:20
A1 Data:824634204160,Len:11,Cap:20
複製程式碼

這下大家明白了吧,他們的Len不一樣,並不是一個Slice,所以使用append方法並沒有改變原來的A,而是新生成了一個A1,即使Dreamerque這位朋友通過如下程式碼 A = append(A, value) 進行復制,也只是一個mSlice的拷貝A的指向被改變了,而且這個A只在Append方法內有效,mSlice本身並沒有改變,所以輸出的mSlice不會有任何變化。

這裡正確的做法是讓Append返回append後的結果。其實對於內建函式append的使用,Go語言(golang)官方做了說明的,要儲存返回的值。

Append returns the updated slice. It is therefore necessary to store the result of append

以上Dreamerque這位朋友的例子中,設定的Len是10,Cap是20,因為Cap足夠大,所以內建函式append並沒有生成新的底層陣列,現在我們把Cap改為10。

type Slice []int

func (A Slice)Append(value int) {
	A1 := append(A, value)

	sh:=(*reflect.SliceHeader)(unsafe.Pointer(&A))
	fmt.Printf("A Data:%d,Len:%d,Cap:%d\n",sh.Data,sh.Len,sh.Cap)

	sh1:=(*reflect.SliceHeader)(unsafe.Pointer(&A1))
	fmt.Printf("A1 Data:%d,Len:%d,Cap:%d\n",sh1.Data,sh1.Len,sh1.Cap)
}

func main() {
	mSlice := make(Slice, 10, 10)
	mSlice.Append(5)
	fmt.Println(mSlice)
}
複製程式碼

執行程式碼我們會發現兩個Slice的Data不再一樣了。

A  Data:824633835680,Len:10,Cap:10
A1 Data:824634204160,Len:11,Cap:20
複製程式碼

這是因為在append的時候,發現Cap不夠,生成了一個新的Data陣列,用於儲存新的資料,並且同時擴充了Cap容量。

小結

最終,我重新回覆了Dreamerque,並對Weelin做了感謝,然後想到這類問題,可以還有不少朋友會遇到,所以寫了一篇文章分析下Slice的本質,也就是SliceHeader,希望可以幫到大家,Go語言,golang ,的確夠浪,SliceHeader很溜。

本文為原創文章,轉載註明出處,歡迎掃碼關注公眾號flysnow_org或者網站www.flysnow.org/,第一時間看後續精彩文章。覺得好的話,請猛擊文章右下角「好看」,感謝支援。

掃碼關注

相關文章