日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網(wǎng)站首頁 編程語言 正文

GoLang?unsafe包詳細講解_Golang

作者:~龐貝 ? 更新時間: 2022-11-23 編程語言

1.前言

開發(fā)中,[]byte類型和string類型需要互相轉換的場景并不少見,直接的想法是像下面這樣進行強制類型轉換:

    a := "Kylin Lab"
	b := []byte(a)
	fmt.Println(a)//Kylin Lab
	fmt.Println(b)//[75 121 108 105 110 32 76 97 98]

如果接下來需要對b進行修改,那么這樣轉換就沒什么問題,但是如果只是因為類型不合適,并不需要對轉換后的變量做任何修改,那這樣轉換就顯得不劃算了。我們知道,[]byte和string的內(nèi)存布局如下圖所示:

可以看到它們都有一個底層數(shù)組來存儲變量數(shù)據(jù),而類型本身只記錄這個數(shù)組的起始地址。如果采用強制類型轉換的方式把a轉換為b,那么就會重新分配b使用的底層數(shù)組。然后把a的底層數(shù)組內(nèi)容拷貝到b的底層數(shù)組。如果字符串內(nèi)容很多,多占用這許多字節(jié)的內(nèi)存不說,還要耗費時間做拷貝,所以就顯得很不合適了。

要是可以讓b重復使用a的底層數(shù)組,那就好了。強轉不行,就到了unsafe上場的時候了~

2.指針類型轉換

unsafe提供的第一件法寶就是指針類型轉換。我們知道像下面這樣的指針類型轉換是編譯不通過的。

a := "Kylin Lab"
var b []byte
tmp := (*string)(&b)
//cannot convert &b (type *[]byte) to type *string

但是你可以把任意一個指針類型轉換為unsafe.Pointer類型,再把unsafe.Pointer類型轉換為任意指針類型,就像下面這樣是可以正常執(zhí)行的:

tmp := (*string)(unsafe.Pointer(&b))

現(xiàn)在我們通過unsafe.Pointer把b的指針轉換為*string類型,我們可以放心的這樣做,是因為我們知道slice的底層布局與string是兼容的,b的前兩項內(nèi)容與a相同,都是一個uintptr和一個int。可參見reflect包中關于這兩個類型的定義:

//reflect/value.go
type StringHeader struct {
    Data uintptr
    Len  int
}
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

我們知道上面這個例子中 變量b只初始化了變量結構,并未初始化底層數(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中重復使用了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可以和各種指針類型相互轉換,也可以轉換為uintptr類型,uintptr本質(zhì)上就是一個無符號整型,所以它是可以進行運算的。 繼續(xù)上面的例子,我們可以把b的指針轉換為unsafe.Pointer,再進一步轉換為uintptr。

(uintptr)(unsafe.Pointer(&b))

現(xiàn)在就把b的地址轉換為uintptr類型了,64位下,如果把它加上16,就是b的容量的起始地址了。

(uintptr)(unsafe.Pointer(&b)) + 16

即便如此,我們也不能直接通過uintptr來修改b的容量,因為它不是指針類型,而且也不能直接轉換為指針類型。但是可以通過unsafe.Pointer類型中轉一下。

tmp2 := (*int)(unsafe.Pointer((uintptr)(unsafe.Pointer(&b)) + 16))

現(xiàn)在才算是拿到了b的容量的指針,再通過這個*int修改b的容量就OK了~

*tmp2 = len(b)

目前為止,我們已經(jīng)借助unsafe的兩個法寶,成功完成了string到[]byte的轉換,并且復用了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類型的臨時變量來存儲一個地址,然后才把它轉換為某個指針類型。

tmp2 := (uintptr)(unsafe.Pointer(&b)) + 16
capPtr := (*int)(unsafe.Pointer(tmp2))

這是因為uintptr只是一個存儲著地址的無符號整型而已,它不是指針,如果垃圾回收為了減少內(nèi)存碎片而移動了一些變量,內(nèi)存關聯(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方法可以獲得結構體中某個字段距離結構體起始地址的偏移值,這樣就可以確定結構體成員正確的位置了。

為了試試這個方法,我們要把b的指針轉換為reflect.SliceHeader類型,其實也可以自己定義一個SliceHeader類型,但這不是有現(xiàn)成的可以直接拿來用嘛~

bPtr := (*reflect.SliceHeader)(unsafe.Pointer(&b)) 

然后獲取Cap字段在結構體內(nèi)的偏移值:

unsafe.Offsetof(bPtr.Cap)

再然后,就是把這個字段的地址轉換為*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轉換為reflect.SliceHeader結構體了,改個字段值哪里要這么麻煩!!!我們大可以這樣做:

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.關于string

關于string,我們還要啰嗦一點,Go語言中string變量的內(nèi)容默認是不會被修改的,而我們通過給string變量整體賦新值的方式來改變它的內(nèi)容時,實際上會重新分配它的底層數(shù)組。

而string類型字面量的底層數(shù)組會被分配到只讀數(shù)據(jù)段,在我們的例子中,b復用了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復用這個字符串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

原文鏈接:https://blog.csdn.net/qq_53267860/article/details/126829492

欄目分類
最近更新