<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在 Go 語言中總是有一些看上去奇奇怪怪的東西,咋一眼一看感覺很熟悉,但又不理解其在 Go 程式碼中的實際意義,面試官卻愛問...
今天要給大家介紹的是 SliceHeader 和 StringHeader 結構體,瞭解清楚他到底是什麼,又有什麼用,並且會在最後給大家介紹 0 拷貝轉換的內容。
一起愉快地開始吸魚之路。
SliceHeader 如其名,Slice + Header,看上去很直觀,實際上是 Go Slice(切片)的執行時表現。
SliceHeader 的定義如下:
type SliceHeader struct { Data uintptr Len int Cap int }
既然知道了切片的執行時表現,那是不是就意味著我們可以自己造一個?
在日常程式中,可以利用標準庫 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 分別是兩個切片。
為什麼兩個新切片所指向的 Data 是同一個地址的呢?
這其實是 Go 語言本身為了減少記憶體佔用,提高整體的效能才這麼設計的。
將切片複製到任意函數的時候,對底層陣列大小都不會影響。複製時只會複製切片本身(值傳遞),不會涉及底層陣列。
也就是在函數間傳遞切片,其只拷貝 24 個位元組(指標欄位 8 個位元組,長度和容量分別需要 8 個位元組),效率很高。
這種設計也引出了新的問題,在平時通過 s[i:j]
所生成的新切片,兩個切片底層指向的是同一個底層陣列。
假設在沒有超過容量(cap)的情況下,對第二個切片操作會影響第一個切片。
這是很多 Go 開發常會碰到的一個大 “坑”,不清楚的排查了很久的都不得而終。
除了 SliceHeader 外,Go 語言中還有一個典型代表,那就是字串(string)的執行時表現。
StringHeader 的定義如下:
type StringHeader struct { Data uintptr Len int }
可得知 “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 語言中,字串都是唯讀的,為了節省記憶體,相同字面量的字串通常對應於同一字串常數,因此指向同一個底層陣列。
為什麼會有人關注到 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結構體SliceHeader及StringHeader作用詳解的詳細內容,更多關於Go結構體SliceHeader StringHeader的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45