Go語言slice的本質-SliceHeader

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

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

留言原由

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

有個問題困擾: 考慮將slice這種引用類型做爲自定義接受者,並綁定方法以下,bash

問題: 此時的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的區別 這篇文章 。app

而後就在前兩天,我收到了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/,第一時間看後續精彩文章。以爲好的話,請猛擊文章右下角「好看」,感謝支持。

掃碼關注
相關文章
相關標籤/搜索