搞 Go 要了解的 2 個 Header,你知道嗎?
大家好,我是煎魚。
在 Go 語言中總是有一些看上去奇奇怪怪的東西,咋一眼一看感覺很熟悉,但又不理解其在 Go 代碼中的實際意義,面試官卻愛問...
今天要給大家介紹的是 SliceHeader 和 StringHeader 結構體,瞭解清楚他到底是什麼,又有什麼用,並且會在最後給大家介紹 0 拷貝轉換的內容。
一起愉快地開始吸魚之路。
SliceHeader
SliceHeader 如其名,Slice + Header,看上去很直觀,實際上是 Go Slice(切片)的運行時表現。
SliceHeader 的定義如下:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
-
Data:指向具體的底層數組。
-
Len:代表切片的長度。
-
Cap:代表切片的容量。
既然知道了切片的運行時表現,那是不是就意味着我們可以自己造一個?
在日常程序中,可以利用標準庫 reflect
提供的 SliceHeader
結構體造一個:
func main() {
// 初始化底層數組
s := [4]string{"腦子", "進", "煎魚", "了"}
s1 := s[0:1]
s2 := s[:]
// 構造 SliceHeader
sh1 := (*reflect.SliceHeader)(unsafe.Pointer(&s1))
sh2 := (*reflect.SliceHeader)(unsafe.Pointer(&s2))
fmt.Println(sh1.Len, sh1.Cap, sh1.Data)
fmt.Println(sh2.Len, sh2.Cap, sh2.Data)
}
你認爲輸出結果是什麼,這兩個新切片會指向同一個底層數組的內存地址嗎?
輸出結果:
1 4 824634330936
4 4 824634330936
兩個切片的 Data 屬性所指向的底層數組是一致的,Len 屬性的值不一樣,sh1 和 sh2 分別是兩個切片。
疑問
坑
這種設計也引出了新的問題,在平時通過 s[i:j]
所生成的新切片,兩個切片底層指向的是同一個底層數組。
假設在沒有超過容量(cap)的情況下,對第二個切片操作會影響第一個切片。
這是很多 Go 開發常會碰到的一個大 “坑”,不清楚的排查了很久的都不得而終。
除了 SliceHeader 外,Go 語言中還有一個典型代表,那就是字符串(string)的運行時表現。
StringHeader 的定義如下:
type StringHeader struct {
Data uintptr
Len int
}
-
Data:存放指針,其指向具體的存儲數據的內存區域。
-
Len:字符串的長度。
可得知 “Hello” 字符串的底層數據如下:
var data = [...]byte{
'h', 'e', 'l', 'l', 'o',
}
底層的存儲示意圖如下:
圖來自網絡
真實演示例子如下:
func main() {
s := "腦子進煎魚了"
s1 := "腦子進煎魚了"
s2 := "腦子進煎魚了"[7:]
fmt.Printf("%d \n", (*reflect.StringHeader)(unsafe.Pointer(&s)).Data)
fmt.Printf("%d \n", (*reflect.StringHeader)(unsafe.Pointer(&s1)).Data)
fmt.Printf("%d \n", (*reflect.StringHeader)(unsafe.Pointer(&s2)).Data)
}
你認爲輸出結果是什麼,變量 s 和 s1、s2 會指向同一個底層內存空間嗎?
輸出結果:
17608227
17608227
17608234
從輸出結果來看,變量 s 和 s1 指向同一個內存地址。變量 s2 雖稍有偏差,但本質上也是指向同一塊。
因爲其是字符串的切片操作,是從第 7 位索引開始,因此正好的 17608234-17608227 = 7。也就是三個變量都是指向同一塊內存空間,這是爲什麼呢?
這是因爲在 Go 語言中,字符串都是隻讀的,爲了節省內存,相同字面量的字符串通常對應於同一字符串常量,因此指向同一個底層數組。
0 拷貝轉換
爲什麼會有人關注到 SliceHeader、StringHeader 這類運行時細節呢,一大部分原因是業內會有開發者,希望利用其實現零拷貝的 string 到 bytes 的轉換。
常見轉換代碼如下:
func string2bytes(s string) []byte {
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: stringHeader.Data,
Len: stringHeader.Len,
Cap: stringHeader.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}
但這其實是錯誤的,官方明確表示:
the Data field is not sufficient to guarantee the data it references will not be garbage collected, so programs must keep a separate, correctly typed pointer to the underlying data.
SliceHeader、StringHeader 的 Data 字段是一個 uintptr
類型。由於 Go 語言只有值傳遞。
因此在上述代碼中會出現將 Data
作爲值拷貝的情況,這就會導致無法保證它所引用的數據不會被垃圾回收(GC)。
應該使用如下轉換方式:
func main() {
s := "腦子進煎魚了"
v := string2bytes1(s)
fmt.Println(v)
}
func string2bytes1(s string) []byte {
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
var b []byte
pbytes := (*reflect.SliceHeader)(unsafe.Pointer(&b))
pbytes.Data = stringHeader.Data
pbytes.Len = stringHeader.Len
pbytes.Cap = stringHeader.Len
return b
}
在程序必須保留一個單獨的、正確類型的指向底層數據的指針。
在性能方面,若只是期望單純的轉換,對容量(cap)等字段值不敏感,也可以使用以下方式:
func string2bytes2(s string) []byte {
return *(*[]byte)(unsafe.Pointer(&s))
}
性能對比:
string2bytes1-1000-4 3.746 ns/op 0 allocs/op
string2bytes1-1000-4 3.713 ns/op 0 allocs/op
string2bytes1-1000-4 3.969 ns/op 0 allocs/op
string2bytes2-1000-4 2.445 ns/op 0 allocs/op
string2bytes2-1000-4 2.451 ns/op 0 allocs/op
string2bytes2-1000-4 2.455 ns/op 0 allocs/op
會相當標準的轉換性能會稍快一些,這種強轉也會導致一個小問題。
代碼如下:
func main() {
s := "腦子進煎魚了"
v := string2bytes2(s)
println(len(v), cap(v))
}
func string2bytes2(s string) []byte {
return *(*[]byte)(unsafe.Pointer(&s))
}
輸出結果:
18 824633927632
這種強轉其會導致 byte 的切片容量非常大,需要特別注意。一般還是推薦使用標準的 SliceHeader、StringHeader 方式就好了,也便於後來的維護者理解。
總結
在這篇文章中,我們介紹了字符串(string)和切片(slice)的兩個運行時表現,分別是 StringHeader 和 SliceHeader。
同時瞭解到其運行時表現後,我們還針對其兩者的地址指向,常見坑進行了說明。
最後我們進一步深入,面向 0 拷貝轉換的場景進行了介紹和性能分析。
你平時有沒有遇到過這塊的疑惑或問題呢,歡迎大家一起討論!
參考
-
Go 語言 slice 的本質 - SliceHeader
-
數組、字符串和切片
-
零拷貝實現 string 和 bytes 的轉換疑問
關注煎魚,吸取他的知識 👆
你好,我是煎魚。高一折騰過前端,參加過國賽拿了獎,大學搞過 PHP。現在整 Go,在公司負責微服務架構等相關工作推進和研發。
從大學開始靠自己賺生活費和學費,到出版 Go 暢銷書《Go 語言編程之旅》,再到獲得 GOP(Go 領域最有觀點專家)榮譽,點擊藍字查看我的出書之路。
日常分享高質量文章,輸出 Go 面試、工作經驗、架構設計,加微信拉讀者交流羣,記得點贊!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/rGqM1wMlqQEoJSgyrgZNLg