Go結(jié)構(gòu)體SliceHeader及StringHeader作用詳解
引言
在 Go 語言中總是有一些看上去奇奇怪怪的東西,咋一眼一看感覺很熟悉,但又不理解其在 Go 代碼中的實際意義,面試官卻愛問...
今天要給大家介紹的是 SliceHeader 和 StringHeader 結(jié)構(gòu)體,了解清楚他到底是什么,又有什么用,并且會在最后給大家介紹 0 拷貝轉(zhuǎn)換的內(nèi)容。
一起愉快地開始吸魚之路。
SliceHeader
SliceHeader 如其名,Slice + Header,看上去很直觀,實際上是 Go Slice(切片)的運行時表現(xiàn)。
SliceHeader 的定義如下:
type?SliceHeader?struct?{ ?Data?uintptr ?Len??int ?Cap??int }
- Data:指向具體的底層數(shù)組。
- Len:代表切片的長度。
- Cap:代表切片的容量。
既然知道了切片的運行時表現(xiàn),那是不是就意味著我們可以自己造一個?
在日常程序中,可以利用標準庫 reflect
提供的 SliceHeader
結(jié)構(gòu)體造一個:
func?main()?{ ??//?初始化底層數(shù)組 ?s?:=?[4]string{"腦子",?"進",?"煎魚",?"了"} ?s1?:=?s[0:1] ?s2?:=?s[:] ??//?構(gòu)造?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) }
你認為輸出結(jié)果是什么,這兩個新切片會指向同一個底層數(shù)組的內(nèi)存地址嗎?
輸出結(jié)果:
1 4 824634330936
4 4 824634330936
兩個切片的 Data 屬性所指向的底層數(shù)組是一致的,Len 屬性的值不一樣,sh1 和 sh2 分別是兩個切片。
疑問
為什么兩個新切片所指向的 Data 是同一個地址的呢?
這其實是 Go 語言本身為了減少內(nèi)存占用,提高整體的性能才這么設(shè)計的。
將切片復(fù)制到任意函數(shù)的時候,對底層數(shù)組大小都不會影響。復(fù)制時只會復(fù)制切片本身(值傳遞),不會涉及底層數(shù)組。
也就是在函數(shù)間傳遞切片,其只拷貝 24 個字節(jié)(指針字段 8 個字節(jié),長度和容量分別需要 8 個字節(jié)),效率很高。
坑
這種設(shè)計也引出了新的問題,在平時通過 s[i:j]
所生成的新切片,兩個切片底層指向的是同一個底層數(shù)組。
假設(shè)在沒有超過容量(cap)的情況下,對第二個切片操作會影響第一個切片。
這是很多 Go 開發(fā)常會碰到的一個大 “坑”,不清楚的排查了很久的都不得而終。
StringHeader
除了 SliceHeader 外,Go 語言中還有一個典型代表,那就是字符串(string)的運行時表現(xiàn)。
StringHeader 的定義如下:
type?StringHeader?struct?{ ???Data?uintptr ???Len??int }
- Data:存放指針,其指向具體的存儲數(shù)據(jù)的內(nèi)存區(qū)域。
- Len:字符串的長度。
可得知 “Hello” 字符串的底層數(shù)據(jù)如下:
var?data?=?[...]byte{ ????'h',?'e',?'l',?'l',?'o', }
底層的存儲示意圖如下:
圖來自網(wǎng)絡(luò)
真實演示例子如下:
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) }
你認為輸出結(jié)果是什么,變量 s 和 s1、s2 會指向同一個底層內(nèi)存空間嗎?
輸出結(jié)果:
17608227
17608227
17608234
從輸出結(jié)果來看,變量 s 和 s1 指向同一個內(nèi)存地址。變量 s2 雖稍有偏差,但本質(zhì)上也是指向同一塊。
因為其是字符串的切片操作,是從第 7 位索引開始,因此正好的 17608234-17608227 = 7。也就是三個變量都是指向同一塊內(nèi)存空間,這是為什么呢?
這是因為在 Go 語言中,字符串都是只讀的,為了節(jié)省內(nèi)存,相同字面量的字符串通常對應(yīng)于同一字符串常量,因此指向同一個底層數(shù)組。
0 拷貝轉(zhuǎn)換
為什么會有人關(guān)注到 SliceHeader、StringHeader 這類運行時細節(jié)呢,一大部分原因是業(yè)內(nèi)會有開發(fā)者,希望利用其實現(xiàn)零拷貝的 string 到 bytes 的轉(zhuǎn)換。
常見轉(zhuǎn)換代碼如下:
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 語言只有值傳遞。
因此在上述代碼中會出現(xiàn)將 Data
作為值拷貝的情況,這就會導(dǎo)致無法保證它所引用的數(shù)據(jù)不會被垃圾回收(GC)。
應(yīng)該使用如下轉(zhuǎn)換方式:
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 }
在程序必須保留一個單獨的、正確類型的指向底層數(shù)據(jù)的指針。
在性能方面,若只是期望單純的轉(zhuǎn)換,對容量(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
會相當標準的轉(zhuǎn)換性能會稍快一些,這種強轉(zhuǎn)也會導(dǎo)致一個小問題。
代碼如下:
func?main()?{ ?s?:=?"腦子進煎魚了" ?v?:=?string2bytes2(s) ?println(len(v),?cap(v)) } func?string2bytes2(s?string)?[]byte?{ ?return?*(*[]byte)(unsafe.Pointer(&s)) }
輸出結(jié)果:
18 824633927632
這種強轉(zhuǎn)其會導(dǎo)致 byte 的切片容量非常大,需要特別注意。一般還是推薦使用標準的 SliceHeader、StringHeader 方式就好了,也便于后來的維護者理解。
總結(jié)
在這篇文章中,我們介紹了字符串(string)和切片(slice)的兩個運行時表現(xiàn),分別是 StringHeader 和 SliceHeader。
同時了解到其運行時表現(xiàn)后,我們還針對其兩者的地址指向,常見坑進行了說明。
最后我們進一步深入,面向 0 拷貝轉(zhuǎn)換的場景進行了介紹和性能分析。
參考
以上就是Go結(jié)構(gòu)體SliceHeader及StringHeader作用詳解的詳細內(nèi)容,更多關(guān)于Go結(jié)構(gòu)體SliceHeader StringHeader的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang使用viper加載配置文件實現(xiàn)自動反序列化到結(jié)構(gòu)
這篇文章主要為大家介紹了golang使用viper加載配置文件實現(xiàn)自動反序列化到結(jié)構(gòu)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08GoLang RabbitMQ實現(xiàn)六種工作模式示例
這篇文章主要介紹了GoLang RabbitMQ實現(xiàn)六種工作模式,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-12-12Goland 2020或2019軟件版本去掉a...或fmt...提示的方法
這篇文章主要介紹了Goland 2020或2019軟件版本去掉a...或fmt...提示的方法,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學(xué)習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-10-10Go語言使用defer+recover解決panic導(dǎo)致程序崩潰的問題
如果協(xié)程出現(xiàn)了panic,就會造成程序的崩潰,這時可以在goroutine中使用recover來捕獲panic,進行處理,本文就詳細的介紹一下,感興趣的可以了解一下2021-09-09