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