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,來看看A
和A1
內部具體的欄位值,這樣來判斷他們是否一致,我們修改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
類型後,我們分別輸出他們的Data
、Len
、Cap
欄位,現在我們看看輸出的結果。
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/,第一時間看後續精彩文章。覺得好的話,請猛擊文章右下角「好看」,感謝支援。