新聞中心
本文轉(zhuǎn)載自微信公眾號(hào)「腦子進(jìn)煎魚了」,作者陳煎魚。轉(zhuǎn)載本文請(qǐng)聯(lián)系腦子進(jìn)煎魚了公眾號(hào)。

成都創(chuàng)新互聯(lián)公司主要從事網(wǎng)站制作、成都網(wǎng)站制作、網(wǎng)頁(yè)設(shè)計(jì)、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)新民,十年網(wǎng)站建設(shè)經(jīng)驗(yàn),價(jià)格優(yōu)惠、服務(wù)專業(yè),歡迎來(lái)電咨詢建站服務(wù):13518219792
大家好,我是煎魚。
在 Go 語(yǔ)言中總是有一些看上去奇奇怪怪的東西,咋一眼一看感覺很熟悉,但又不理解其在 Go 代碼中的實(shí)際意義,面試官卻愛問(wèn)...
今天要給大家介紹的是 SliceHeader 和 StringHeader 結(jié)構(gòu)體,了解清楚他到底是什么,又有什么用,并且會(huì)在最后給大家介紹 0 拷貝轉(zhuǎn)換的內(nèi)容。
一起愉快地開始吸魚之路。
SliceHeader
SliceHeader 如其名,Slice + Header,看上去很直觀,實(shí)際上是 Go Slice(切片)的運(yùn)行時(shí)表現(xiàn)。
- type SliceHeader struct {
- Data uintptr
- Len int
- Cap int
- }
- Data:指向具體的底層數(shù)組。
- Len:代表切片的長(zhǎng)度。
- Cap:代表切片的容量。
既然知道了切片的運(yùn)行時(shí)表現(xiàn),那是不是就意味著我們可以自己造一個(gè)?
在日常程序中,可以利用標(biāo)準(zhǔn)庫(kù) reflect 提供的 SliceHeader 結(jié)構(gòu)體造一個(gè):
- func main() {
- // 初始化底層數(shù)組
- s := [4]string{"腦子", "進(jìn)", "煎魚", "了"}
- 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)
- }
你認(rèn)為輸出結(jié)果是什么,這兩個(gè)新切片會(huì)指向同一個(gè)底層數(shù)組的內(nèi)存地址嗎?
輸出結(jié)果:
- 1 4 824634330936
- 4 4 824634330936
兩個(gè)切片的 Data 屬性所指向的底層數(shù)組是一致的,Len 屬性的值不一樣,sh1 和 sh2 分別是兩個(gè)切片。
疑問(wèn)
為什么兩個(gè)新切片所指向的 Data 是同一個(gè)地址的呢?
這其實(shí)是 Go 語(yǔ)言本身為了減少內(nèi)存占用,提高整體的性能才這么設(shè)計(jì)的。
將切片復(fù)制到任意函數(shù)的時(shí)候,對(duì)底層數(shù)組大小都不會(huì)影響。復(fù)制時(shí)只會(huì)復(fù)制切片本身(值傳遞),不會(huì)涉及底層數(shù)組。
也就是在函數(shù)間傳遞切片,其只拷貝 24 個(gè)字節(jié)(指針字段 8 個(gè)字節(jié),長(zhǎng)度和容量分別需要 8 個(gè)字節(jié)),效率很高。
坑
這種設(shè)計(jì)也引出了新的問(wèn)題,在平時(shí)通過(guò) s[i:j] 所生成的新切片,兩個(gè)切片底層指向的是同一個(gè)底層數(shù)組。
假設(shè)在沒(méi)有超過(guò)容量(cap)的情況下,對(duì)第二個(gè)切片操作會(huì)影響第一個(gè)切片。
這是很多 Go 開發(fā)常會(huì)碰到的一個(gè)大 “坑”,不清楚的排查了很久的都不得而終。
StringHeader
除了 SliceHeader 外,Go 語(yǔ)言中還有一個(gè)典型代表,那就是字符串(string)的運(yùn)行時(shí)表現(xiàn)。
- type StringHeader struct {
- Data uintptr
- Len int
- }
- Data:存放指針,其指向具體的存儲(chǔ)數(shù)據(jù)的內(nèi)存區(qū)域。
- Len:字符串的長(zhǎng)度。
可得知 “Hello” 字符串的底層數(shù)據(jù)如下:
- var data = [...]byte{
- 'h', 'e', 'l', 'l', 'o',
- }
底層的存儲(chǔ)示意圖如下:
真實(shí)演示例子如下:
- func main() {
- s := "腦子進(jìn)煎魚了"
- s1 := "腦子進(jìn)煎魚了"
- s2 := "腦子進(jìn)煎魚了"[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)
- }
你認(rèn)為輸出結(jié)果是什么,變量 s 和 s1、s2 會(huì)指向同一個(gè)底層內(nèi)存空間嗎?
輸出結(jié)果:
- 17608227
- 17608227
- 17608234
從輸出結(jié)果來(lái)看,變量 s 和 s1 指向同一個(gè)內(nèi)存地址。變量 s2 雖稍有偏差,但本質(zhì)上也是指向同一塊。
因?yàn)槠涫亲址那衅僮?,是從?7 位索引開始,因此正好的 17608234-17608227 = 7。也就是三個(gè)變量都是指向同一塊內(nèi)存空間,這是為什么呢?
這是因?yàn)樵?Go 語(yǔ)言中,字符串都是只讀的,為了節(jié)省內(nèi)存,相同字面量的字符串通常對(duì)應(yīng)于同一字符串常量,因此指向同一個(gè)底層數(shù)組。
0 拷貝轉(zhuǎn)換
為什么會(huì)有人關(guān)注到 SliceHeader、StringHeader 這類運(yùn)行時(shí)細(xì)節(jié)呢,一大部分原因是業(yè)內(nèi)會(huì)有開發(fā)者,希望利用其實(shí)現(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))
- }
但這其實(shí)是錯(cuò)誤的,官方明確表示:
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 字段是一個(gè) uintptr 類型。由于 Go 語(yǔ)言只有值傳遞。
因此在上述代碼中會(huì)出現(xiàn)將 Data 作為值拷貝的情況,這就會(huì)導(dǎo)致無(wú)法保證它所引用的數(shù)據(jù)不會(huì)被垃圾回收(GC)。
應(yīng)該使用如下轉(zhuǎn)換方式:
- func main() {
- s := "腦子進(jìn)煎魚了"
- 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
- }
在程序必須保留一個(gè)單獨(dú)的、正確類型的指向底層數(shù)據(jù)的指針。
在性能方面,若只是期望單純的轉(zhuǎn)換,對(duì)容量(cap)等字段值不敏感,也可以使用以下方式:
- func string2bytes2(s string) []byte {
- return *(*[]byte)(unsafe.Pointer(&s))
- }
性能對(duì)比:
- 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
會(huì)相當(dāng)標(biāo)準(zhǔn)的轉(zhuǎn)換性能會(huì)稍快一些,這種強(qiáng)轉(zhuǎn)也會(huì)導(dǎo)致一個(gè)小問(wèn)題。
代碼如下:
- func main() {
- s := "腦子進(jìn)煎魚了"
- v := string2bytes2(s)
- println(len(v), cap(v))
- }
- func string2bytes2(s string) []byte {
- return *(*[]byte)(unsafe.Pointer(&s))
- }
輸出結(jié)果:
- 18 824633927632
這種強(qiáng)轉(zhuǎn)其會(huì)導(dǎo)致 byte 的切片容量非常大,需要特別注意。一般還是推薦使用標(biāo)準(zhǔn)的 SliceHeader、StringHeader 方式就好了,也便于后來(lái)的維護(hù)者理解。
總結(jié)
在這篇文章中,我們介紹了字符串(string)和切片(slice)的兩個(gè)運(yùn)行時(shí)表現(xiàn),分別是 StringHeader 和 SliceHeader。
同時(shí)了解到其運(yùn)行時(shí)表現(xiàn)后,我們還針對(duì)其兩者的地址指向,常見坑進(jìn)行了說(shuō)明。
最后我們進(jìn)一步深入,面向 0 拷貝轉(zhuǎn)換的場(chǎng)景進(jìn)行了介紹和性能分析。
你平時(shí)有沒(méi)有遇到過(guò)這塊的疑惑或問(wèn)題呢,歡迎大家一起討論!
參考
Go語(yǔ)言slice的本質(zhì)-SliceHeader
數(shù)組、字符串和切片
零拷貝實(shí)現(xiàn)string 和bytes的轉(zhuǎn)換疑問(wèn)
網(wǎng)頁(yè)名稱:搞Go要了解的2個(gè)Header,你知道嗎?
當(dāng)前鏈接:http://m.fisionsoft.com.cn/article/cdoceod.html


咨詢
建站咨詢
