GoLang unsafe包詳細講解
1.前言
開發(fā)中,[]byte類型和string類型需要互相轉(zhuǎn)換的場景并不少見,直接的想法是像下面這樣進行強制類型轉(zhuǎn)換:
a := "Kylin Lab" b := []byte(a) fmt.Println(a)//Kylin Lab fmt.Println(b)//[75 121 108 105 110 32 76 97 98]
如果接下來需要對b進行修改,那么這樣轉(zhuǎn)換就沒什么問題,但是如果只是因為類型不合適,并不需要對轉(zhuǎn)換后的變量做任何修改,那這樣轉(zhuǎn)換就顯得不劃算了。我們知道,[]byte和string的內(nèi)存布局如下圖所示:
可以看到它們都有一個底層數(shù)組來存儲變量數(shù)據(jù),而類型本身只記錄這個數(shù)組的起始地址。如果采用強制類型轉(zhuǎn)換的方式把a轉(zhuǎn)換為b,那么就會重新分配b使用的底層數(shù)組。然后把a的底層數(shù)組內(nèi)容拷貝到b的底層數(shù)組。如果字符串內(nèi)容很多,多占用這許多字節(jié)的內(nèi)存不說,還要耗費時間做拷貝,所以就顯得很不合適了。
要是可以讓b重復(fù)使用a的底層數(shù)組,那就好了。強轉(zhuǎn)不行,就到了unsafe上場的時候了~
2.指針類型轉(zhuǎn)換
unsafe提供的第一件法寶就是指針類型轉(zhuǎn)換。我們知道像下面這樣的指針類型轉(zhuǎn)換是編譯不通過的。
a := "Kylin Lab" var b []byte tmp := (*string)(&b) //cannot convert &b (type *[]byte) to type *string
但是你可以把任意一個指針類型轉(zhuǎn)換為unsafe.Pointer類型,再把unsafe.Pointer類型轉(zhuǎn)換為任意指針類型,就像下面這樣是可以正常執(zhí)行的:
tmp := (*string)(unsafe.Pointer(&b))
現(xiàn)在我們通過unsafe.Pointer把b的指針轉(zhuǎn)換為*string類型,我們可以放心的這樣做,是因為我們知道slice的底層布局與string是兼容的,b的前兩項內(nèi)容與a相同,都是一個uintptr和一個int??蓞⒁妑eflect包中關(guān)于這兩個類型的定義:
//reflect/value.go type StringHeader struct { Data uintptr Len int } type SliceHeader struct { Data uintptr Len int Cap int }
我們知道上面這個例子中 變量b只初始化了變量結(jié)構(gòu),并未初始化底層數(shù)組,元素個數(shù)和容量都為0。
接下來,我們把a賦值給tmp:
a := "Kylin Lab" var b []byte tmp := (*string)(unsafe.Pointer(&b)) *tmp = a fmt.Println(a) //Kylin Lab fmt.Println(b) //[75 121 108 105 110 32 76 97 98] fmt.Println(*tmp) //Kylin Lab fmt.Println(tmp) //0xc000004078 fmt.Printf("%p\n", &a) //0xc00005a250 fmt.Printf("%p\n", &b) //0xc000004078 fmt.Println(&a) //0xc00005a250 fmt.Println(&b)//&[75 121 108 105 110 32 76 97 98]
現(xiàn)在你猜怎么著,我們已經(jīng)在變量b中重復(fù)使用了a的底層數(shù)組,元素個數(shù)也填好了~
不過還沒完,b的容量還為0呢!怎么修改它呢?我們能拿到b的地址,也知道data和len各占8字節(jié)(64位下),只要把b的指針加上16字節(jié)就是cap的起始地址。可問題是Go語言的指針支持做加減運算嗎?不支持!
這時候就要拿出unsafe提供的第二件法寶了!
a := "Kylin Lab" var b []byte tmp := (*string)(unsafe.Pointer(&b)) *tmp = a fmt.Println(len(a)) //9 fmt.Println(len(b)) //9 fmt.Println(cap(b)) //0
//unsafe/unsafe.go package unsafe type ArbitraryType int type IntegerType int//引用不會出錯 type Pointer *ArbitraryType func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr func Add(ptr Pointer, len IntegerType) Pointer func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
//builtin/builtin.go // uintptr is an integer type that is large enough to hold the bit pattern of // any pointer. type uintptr uintptr // IntegerType is here for the purposes of documentation only. It is a stand-in // for any integer type: int, uint, int8 etc. type IntegerType int//引用會出錯
3.指針運算
Go語言不支持指針直接進行運算,也是為了保障程序運行安全,防止出現(xiàn)莫名其妙的、玄之又玄的bug。
不過unsafe.Pointer可以和各種指針類型相互轉(zhuǎn)換,也可以轉(zhuǎn)換為uintptr類型,uintptr本質(zhì)上就是一個無符號整型,所以它是可以進行運算的。 繼續(xù)上面的例子,我們可以把b的指針轉(zhuǎn)換為unsafe.Pointer,再進一步轉(zhuǎn)換為uintptr。
(uintptr)(unsafe.Pointer(&b))
現(xiàn)在就把b的地址轉(zhuǎn)換為uintptr類型了,64位下,如果把它加上16,就是b的容量的起始地址了。
(uintptr)(unsafe.Pointer(&b)) + 16
即便如此,我們也不能直接通過uintptr來修改b的容量,因為它不是指針類型,而且也不能直接轉(zhuǎn)換為指針類型。但是可以通過unsafe.Pointer類型中轉(zhuǎn)一下。
tmp2 := (*int)(unsafe.Pointer((uintptr)(unsafe.Pointer(&b)) + 16))
現(xiàn)在才算是拿到了b的容量的指針,再通過這個*int修改b的容量就OK了~
*tmp2 = len(b)
目前為止,我們已經(jīng)借助unsafe的兩個法寶,成功完成了string到[]byte的轉(zhuǎn)換,并且復(fù)用了a的底層數(shù)組。
a := "Kylin Lab" var b []byte tmp := (*string)(unsafe.Pointer(&b)) *tmp = a tmp2 := (*int)(unsafe.Pointer((uintptr)(unsafe.Pointer(&b)) + 16)) *tmp2 = len(b) fmt.Println(len(a)) //9 fmt.Println(len(b)) //9 fmt.Println(cap(b)) //9
上面tmp2賦值這一行很長,也很繞。
注:雖然下面可以編譯過,但是一定不要像下面這樣先使用uintptr類型的臨時變量來存儲一個地址,然后才把它轉(zhuǎn)換為某個指針類型。
tmp2 := (uintptr)(unsafe.Pointer(&b)) + 16 capPtr := (*int)(unsafe.Pointer(tmp2))
這是因為uintptr只是一個存儲著地址的無符號整型而已,它不是指針,如果垃圾回收為了減少內(nèi)存碎片而移動了一些變量,內(nèi)存關(guān)聯(lián)到的指針類型的值是會一并修改的,但是uintptr并不會,這就可能出現(xiàn)一些神奇的bug,所以這一行只能這么繞著寫。
除此之外,這個硬編碼的“16”怎么看都顯得格外不和諧。有沒有什么好方法,可以獲取程序運行平臺中一個類型的大小呢?這就要用到unsafe提供的第三個法寶了~
4.獲取大小和偏移
unsafe.Sizeof可以拿到任意類型的大小,unsafe.Alignof可以拿到任意類型的對齊邊界。按照reflect.SliceHeader的定義,我們這里可以用unsafe.Sizeof來獲取uintptr和int的大小,b的起始地址偏移這么多就是第三個字段Cap的地址了。
a := "Kylin Lab" var b []byte tmp := (*string)(unsafe.Pointer(&b)) *tmp = a tmp2 := (*int)(unsafe.Pointer((uintptr)(unsafe.Pointer(&b)) + unsafe.Sizeof(uintptr(1)) + unsafe.Sizeof(1))) *tmp2 = len(b) fmt.Println(len(a)) //9 fmt.Println(len(b)) //9 fmt.Println(cap(b)) //9
不過這樣還是存在投機的成分,別忘了內(nèi)存對齊哦~
這里這樣寫可行,是因為我們知道uintptr和int的大小不是4字節(jié)就是8字節(jié),無論哪一種,都會緊挨著第三個字段,不會出現(xiàn)因內(nèi)存對齊而形成的間隙。
所以unsafe還有一個unsafe.Offsetof方法可以獲得結(jié)構(gòu)體中某個字段距離結(jié)構(gòu)體起始地址的偏移值,這樣就可以確定結(jié)構(gòu)體成員正確的位置了。
為了試試這個方法,我們要把b的指針轉(zhuǎn)換為reflect.SliceHeader類型,其實也可以自己定義一個SliceHeader類型,但這不是有現(xiàn)成的可以直接拿來用嘛~
bPtr := (*reflect.SliceHeader)(unsafe.Pointer(&b))
然后獲取Cap字段在結(jié)構(gòu)體內(nèi)的偏移值:
unsafe.Offsetof(bPtr.Cap)
再然后,就是把這個字段的地址轉(zhuǎn)換為*int,然后修改它的值了:
a := "Kylin Lab" var b []byte tmp := (*string)(unsafe.Pointer(&b)) *tmp = a bPtr := (*reflect.SliceHeader)(unsafe.Pointer(&b)) tmp2 := (*int)(unsafe.Pointer((uintptr)(unsafe.Pointer(&b)) + unsafe.Offsetof(bPtr.Cap))) *tmp2 = len(b) fmt.Println(len(a)) //9 fmt.Println(len(b)) //9 fmt.Println(cap(b)) //9
我們?yōu)榱硕嘟榻B一些unsafe的功能,刻意繞了個遠~
其實都把b轉(zhuǎn)換為reflect.SliceHeader結(jié)構(gòu)體了,改個字段值哪里要這么麻煩!??!我們大可以這樣做:
strHeader := (*reflect.StringHeader)(unsafe.Pointer(&a)) sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
這樣通過strHeader和sliceHeader想操作哪個字段都很方便。
a := "Kylin Lab" var b []byte strHeader := (*reflect.StringHeader)(unsafe.Pointer(&a)) sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b)) sliceHeader.Data = strHeader.Data sliceHeader.Len = strHeader.Len sliceHeader.Cap = strHeader.Len fmt.Println(len(a)) //9 fmt.Println(len(b)) //9 fmt.Println(cap(b)) //9
5.關(guān)于string
關(guān)于string,我們還要啰嗦一點,Go語言中string變量的內(nèi)容默認是不會被修改的,而我們通過給string變量整體賦新值的方式來改變它的內(nèi)容時,實際上會重新分配它的底層數(shù)組。
而string類型字面量的底層數(shù)組會被分配到只讀數(shù)據(jù)段,在我們的例子中,b復(fù)用了a的底層數(shù)組,所以就不能再像下面這樣修改b的內(nèi)容了,否則執(zhí)行階段會發(fā)生錯誤。
a := "Kylin Lab" var b []byte strHeader := (*reflect.StringHeader)(unsafe.Pointer(&a)) sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b)) sliceHeader.Data = strHeader.Data sliceHeader.Len = strHeader.Len sliceHeader.Cap = strHeader.Len b[0] = 'k' /*運行報錯: unexpected fault address 0x6d1875 fatal error: fault [signal 0xc0000005 code=0x1 addr=0x6d1875 pc=0x6c013a]*/
而運行時動態(tài)拼接而成的string變量,它的底層數(shù)組不在只讀數(shù)據(jù)段,而是由Go語言在語法層面阻止對字符串內(nèi)容的修改行為。
a := "Kylin Lab" //string字面量 c := "Hello " + a //動態(tài)拼接的字符串 c[0] = 'h' // cannot assign to c[0] 編譯時報錯
a := "Kylin Lab" //string字面量 a[0] = 'h' // cannot assign to c[0] 編譯時報錯
若我們利用unsafe讓一個[]byte復(fù)用這個字符串c的底層數(shù)組,就可以繞過Go語法層面的限制,修改底層數(shù)組的內(nèi)容了。
但是盡量不要這樣做,如果不確定這個字符串會在哪里用到的話~
a := "Kylin Lab" c := "Hello" + a var s []byte strHeader := (*reflect.StringHeader)(unsafe.Pointer(&c)) sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&s)) sliceHeader.Data = strHeader.Data sliceHeader.Len = strHeader.Len sliceHeader.Cap = strHeader.Len s[0] = 'h' fmt.Println(c) //hello Kylin Lab fmt.Println(a) //Kylin Lab fmt.Println(string(s)) //hello Kylin Lab
到此這篇關(guān)于GoLang unsafe包詳細講解的文章就介紹到這了,更多相關(guān)GoLang unsafe內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang編程并發(fā)工具庫MapReduce使用實踐
這篇文章主要為大家介紹了Golang并發(fā)工具庫MapReduce的使用實踐,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-04-04解決golang sync.Wait()不執(zhí)行的問題
這篇文章主要介紹了解決golang sync.Wait()不執(zhí)行的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12GO語言判斷一個網(wǎng)段是否屬于另一個網(wǎng)段的子網(wǎng)
這篇文章主要介紹了GO語言判斷一個網(wǎng)段是否屬于另一個網(wǎng)段的子網(wǎng)的相關(guān)資料,內(nèi)容介紹詳細,具有一定的參考價值,需要的朋友可任意參考一下2022-03-03go-zero 應(yīng)對海量定時/延遲任務(wù)的技巧
這篇文章主要介紹了go-zero 如何應(yīng)對海量定時/延遲任務(wù),本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-10-10