Go語言slice的本質-SliceHeader

  • 2020 年 2 月 10 日
  • 筆記

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

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

留言起因

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

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

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

1 2 3 4 5 6 7 8 9 10 11

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的指針一樣的啊,怎麼會不是一個呢?我們來測試一次,修改程式碼如下:

1 2 3 4

func (A Slice)Append(value int) { A1 := append(A, value) fmt.Printf("%pn%pn",A,A1) }

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

SliceHeader登場

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

1 2 3 4 5

type SliceHeader struct { Data uintptr Len int Cap int }

正好對應Slice的三要素,Data指向具體的底層數據源數組,Len代表長度,Cap代表容量。

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

1 2 3 4 5 6 7 8 9 10 11 12

//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:%dn",sh.Data,sh.Len,sh.Cap) sh1:=(*reflect.SliceHeader)(unsafe.Pointer(&A1)) fmt.Printf("A1 Data:%d,Len:%d,Cap:%dn",sh1.Data,sh1.Len,sh1.Cap) }

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

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

1 2

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。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

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:%dn",sh.Data,sh.Len,sh.Cap) sh1:=(*reflect.SliceHeader)(unsafe.Pointer(&A1)) fmt.Printf("A1 Data:%d,Len:%d,Cap:%dn",sh1.Data,sh1.Len,sh1.Cap) } func main() { mSlice := make(Slice, 10, 10) mSlice.Append(5) fmt.Println(mSlice) }

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

1 2

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或者網站http://www.flysnow.org/,第一時間看後續精彩文章。覺得好的話,請猛擊文章右下角「好看」,感謝支援。