Go 学习笔记(74)— Go 标准库之 unsafe
Go 語(yǔ)言自帶的 unsafe 包的高級(jí)用法, 顧名思義,unsafe 是不安全的。Go 將其定義為這個(gè)包名,也是為了讓我們盡可能地不使用它。不過(guò)雖然不安全,它也有優(yōu)勢(shì),那就是可以繞過(guò) Go 的內(nèi)存安全機(jī)制,直接對(duì)內(nèi)存進(jìn)行讀寫。所以有時(shí)候出于性能需要,還是會(huì)冒險(xiǎn)使用它來(lái)對(duì)內(nèi)存進(jìn)行操作。
1. 指針類型轉(zhuǎn)換
Go 是一門強(qiáng)類型的靜態(tài)語(yǔ)言。強(qiáng)類型意味著一旦定義了,類型就不能改變;靜態(tài)意味著類型檢查在運(yùn)行前就做了。同時(shí)出于安全考慮,Go 語(yǔ)言是不允許兩個(gè)指針類型進(jìn)行轉(zhuǎn)換的。
我們一般使用 *T 作為一個(gè)指針類型,表示一個(gè)指向類型 T 變量的指針。為了安全的考慮,兩個(gè)不同的指針類型不能相互轉(zhuǎn)換,比如 *int 不能轉(zhuǎn)為 *float64 。
我們來(lái)看下面的代碼:
func main() {i:= 10ip:=&ivar fp *float64 = (*float64)(ip)fmt.Println(fp)
}
這個(gè)代碼在編譯的時(shí)候,會(huì)提示
cannot convert ip (type * int) to type * float64
也就是不能進(jìn)行強(qiáng)制轉(zhuǎn)型。那如果還是需要轉(zhuǎn)換呢?這就需要使用 unsafe 包里的 Pointer 了。
unsafe.Pointer 是一種特殊意義的指針,可以表示任意類型的地址,類似 C 語(yǔ)言里的 void* 指針,是全能型的。
正常情況下,*int 無(wú)法轉(zhuǎn)換為 *float64 ,但是通過(guò) unsafe.Pointer 做中轉(zhuǎn)就可以了。在下面的示例中,通過(guò) unsafe.Pointer 把 *int 轉(zhuǎn)換為 *float64,并且對(duì)新的 *float64 進(jìn)行 3 倍的乘法操作,你會(huì)發(fā)現(xiàn)原來(lái)變量 i 的值也被改變了,變?yōu)?30。
func main() {i := 10ip := &ivar fp *float64 = (*float64)(unsafe.Pointer(ip))*fp = *fp * 3fmt.Println(*ip) // 30
}
說(shuō)明通過(guò) unsafe.Pointer 這個(gè)萬(wàn)能的指針,我們可以在 *T 之間做任何轉(zhuǎn)換。那么 unsafe.Pointer 到底是什么?為什么其他類型的指針可以轉(zhuǎn)換為 unsafe.Pointer 呢?這就要看 unsafe.Pointer 的源代碼定義了,如下所示:
// ArbitraryType is here for the purposes of documentation
// only and is not actually part of the unsafe package.
// It represents the type of an arbitrary Go expression.
type ArbitraryType int
type Pointer *ArbitraryType
按 Go 語(yǔ)言官方的注釋,ArbitraryType 可以表示任何類型(這里的 ArbitraryType 僅僅是文檔需要,不用太關(guān)注它本身,只要記住可以表示任何類型即可)。 而 unsafe.Pointer 又是 *ArbitraryType ,也就是說(shuō) unsafe.Pointer 是任何類型的指針,也就是一個(gè)通用型的指針,足以表示任何內(nèi)存地址。
2. uintptr 指針類型
uintptr 也是一種指針類型,它足夠大,可以表示任何指針。它的類型定義如下所示:
// uintptr is an integer type that is large enough
// to hold the bit pattern of any pointer.
type uintptr uintptr
既然已經(jīng)有了 unsafe.Pointer ,為什么還要設(shè)計(jì) uintptr 類型呢?這是因?yàn)?unsafe.Pointer 不能進(jìn)行運(yùn)算,比如不支持 +(加號(hào))運(yùn)算符操作,但是 uintptr 可以。通過(guò)它,可以對(duì)指針偏移進(jìn)行計(jì)算,這樣就可以訪問(wèn)特定的內(nèi)存,達(dá)到對(duì)特定內(nèi)存讀寫的目的,這是真正內(nèi)存級(jí)別的操作。
在下面的代碼中,通過(guò)指針偏移修改 struct 結(jié)構(gòu)體內(nèi)的字段為例,演示 uintptr 的用法。
func main() {p := new(person)//Name是person的第一個(gè)字段不用偏移,即可通過(guò)指針修改pName := (*string)(unsafe.Pointer(p))*pName = "wohu"//Age并不是person的第一個(gè)字段,所以需要進(jìn)行偏移,這樣才能正確定位到Age字段這塊內(nèi)存,才可以正確的修改pAge := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Offsetof(p.Age)))*pAge = 20fmt.Printf("p is %#v", *p) // p is main.person{Name:"wohu", Age:20}
}type person struct {Name stringAge int
}
這個(gè)示例不是通過(guò)直接訪問(wèn)相應(yīng)字段的方式對(duì) person 結(jié)構(gòu)體字段賦值,而是通過(guò)指針偏移找到相應(yīng)的內(nèi)存,然后對(duì)內(nèi)存操作進(jìn)行賦值。
下面詳細(xì)介紹操作步驟。
-
先使用
new函數(shù)聲明一個(gè)*person類型的指針變量p。 -
然后把
*person類型的指針變量p通過(guò)unsafe.Pointer,轉(zhuǎn)換為*string類型的指針變量pName。 -
因?yàn)?
person這個(gè)結(jié)構(gòu)體的第一個(gè)字段就是string類型的Name,所以pName這個(gè)指針就指向Name字段(偏移為 0),對(duì)pName進(jìn)行修改其實(shí)就是修改字段Name的值。 -
因?yàn)?
Age字段不是person的第一個(gè)字段,要修改它必須要進(jìn)行指針偏移運(yùn)算。所以需要先把指針變量p通過(guò)unsafe.Pointer 轉(zhuǎn)換為uintptr,這樣才能進(jìn)行地址運(yùn)算。
既然要進(jìn)行指針偏移,那么要偏移多少呢?這個(gè)偏移量可以通過(guò)函數(shù) unsafe.Offsetof 計(jì)算出來(lái),該函數(shù)返回的是一個(gè) uintptr 類型的偏移量,有了這個(gè)偏移量就可以通過(guò) + 號(hào)運(yùn)算符獲得正確的 Age 字段的內(nèi)存地址了,也就是通過(guò) unsafe.Pointer 轉(zhuǎn)換后的 *int 類型的指針變量 pAge。
然后需要注意的是,如果要進(jìn)行指針運(yùn)算,要先通過(guò) unsafe.Pointer 轉(zhuǎn)換為 uintptr 類型的指針。指針運(yùn)算完畢后,還要通過(guò) unsafe.Pointer 轉(zhuǎn)換為真實(shí)的指針類型(比如示例中的 *int 類型),這樣可以對(duì)這塊內(nèi)存進(jìn)行賦值或取值操作。
- 有了指向字段
Age的指針變量pAge,就可以對(duì)其進(jìn)行賦值操作,修改字段Age的值了。
這個(gè)示例主要是為了講解 uintptr 指針運(yùn)算,所以一個(gè)結(jié)構(gòu)體字段的賦值才會(huì)寫得這么復(fù)雜,如果按照正常的編碼,以上示例代碼會(huì)和下面的代碼結(jié)果一樣。
func main() {p :=new(person)p.Name = "wohu"p.Age = 20fmt.Println(*p)
}
指針運(yùn)算的核心在于它操作的是一個(gè)個(gè)內(nèi)存地址,通過(guò)內(nèi)存地址的增減,就可以指向一塊塊不同的內(nèi)存并對(duì)其進(jìn)行操作,而且不必知道這塊內(nèi)存被起了什么名字(變量名)。
3. 指針轉(zhuǎn)換規(guī)則
你已經(jīng)知道 Go 語(yǔ)言中存在三種類型的指針,它們分別是:常用的 *T 、unsafe.Pointer 及 uintptr 。通過(guò)以上示例講解,可以總結(jié)出這三者的轉(zhuǎn)換規(guī)則:
- 任何類型的
*T都可以轉(zhuǎn)換為unsafe.Pointer; unsafe.Pointer也可以轉(zhuǎn)換為任何類型的*T;unsafe.Pointer可以轉(zhuǎn)換為uintptr;uintptr也可以轉(zhuǎn)換為unsafe.Pointer;
可以發(fā)現(xiàn),unsafe.Pointer 主要用于指針類型的轉(zhuǎn)換,而且是各個(gè)指針類型轉(zhuǎn)換的橋梁。uintptr 主要用于指針運(yùn)算,尤其是通過(guò)偏移量定位不同的內(nèi)存。
4. unsafe.Sizeof
Sizeof 函數(shù)可以返回一個(gè)類型所占用的內(nèi)存大小,這個(gè)大小只與類型有關(guān),和類型對(duì)應(yīng)的變量存儲(chǔ)的內(nèi)容大小無(wú)關(guān),比如 bool 型占用一個(gè)字節(jié)、int8 也占用一個(gè)字節(jié)。
通過(guò) Sizeof 函數(shù)你可以查看任何類型(比如字符串、切片、整型)占用的內(nèi)存大小,示例代碼如下:
func main() {fmt.Println(unsafe.Sizeof(true)) // 1fmt.Println(unsafe.Sizeof(int8(0))) // 1fmt.Println(unsafe.Sizeof(int16(0))) // 2fmt.Println(unsafe.Sizeof(int32(0))) // 4fmt.Println(unsafe.Sizeof(int64(0))) // 8fmt.Println(unsafe.Sizeof(int(0))) // 8fmt.Println(unsafe.Sizeof(string("張三"))) // 16fmt.Println(unsafe.Sizeof([]string{"李四", "張三"})) // 24
}
對(duì)于整型來(lái)說(shuō),占用的字節(jié)數(shù)意味著這個(gè)類型存儲(chǔ)數(shù)字范圍的大小,比如 int8 占用一個(gè)字節(jié),也就是 8bit,所以它可以存儲(chǔ)的大小范圍是 -128~~127,也就是 ?2^(n-1) 到 2^(n-1)?1 。其中 n 表示 bit,int8 表示 8bit,int16 表示 16bit,以此類推。
對(duì)于和平臺(tái)有關(guān)的 int 類型,要看平臺(tái)是 32 位還是 64 位,會(huì)取最大的。比如我自己測(cè)試以上輸出,會(huì)發(fā)現(xiàn) int 和 int64 的大小是一樣的,因?yàn)槲矣玫氖?64 位平臺(tái)的電腦。
小提示:一個(gè) struct 結(jié)構(gòu)體的內(nèi)存占用大小,等于它包含的字段類型內(nèi)存占用大小之和。
總結(jié):
unsafe 包里最常用的就是 Pointer 指針,通過(guò)它可以讓你在 *T、uintptr 及 Pointer 三者間轉(zhuǎn)換,從而實(shí)現(xiàn)自己的需求,比如零內(nèi)存拷貝或通過(guò) uintptr 進(jìn)行指針運(yùn)算,這些都可以提高程序效率。
unsafe 包里的功能雖然不安全,但的確很香,比如指針運(yùn)算、類型轉(zhuǎn)換等,都可以幫助我們提高性能。不過(guò)我還是建議盡可能地不使用,因?yàn)樗梢岳@開 Go 語(yǔ)言編譯器的檢查,可能會(huì)因?yàn)槟愕牟僮魇д`而出現(xiàn)問(wèn)題。當(dāng)然如果是需要提高性能的必要操作,還是可以使用,比如 []byte 轉(zhuǎn) string,就可以通過(guò) unsafe.Pointer 實(shí)現(xiàn)零內(nèi)存拷貝。
5. uintptr 和 unsafe.Pointer 的區(qū)別
unsafe.Pointer只是單純的通用指針類型,用于轉(zhuǎn)換不同類型指針,它不可以參與指針運(yùn)算;- 而
uintptr是用于指針運(yùn)算的,GC不把uintptr當(dāng)指針,也就是說(shuō)uintptr無(wú)法持有對(duì)象,uintptr類型的目標(biāo)會(huì)被回收; unsafe.Pointer可以和 普通指針 進(jìn)行相互轉(zhuǎn)換;unsafe.Pointer可以和uintptr進(jìn)行相互轉(zhuǎn)換;
package mainimport ("fmt""unsafe"
)type W struct {b int32c int64
}func main() {var w *W = new(W)//這時(shí)w的變量打印出來(lái)都是默認(rèn)值0,0fmt.Println(w.b, w.c)//現(xiàn)在我們通過(guò)指針運(yùn)算給b變量賦值為10b := unsafe.Pointer(uintptr(unsafe.Pointer(w)) + unsafe.Offsetof(w.b))*((*int)(b)) = 10//此時(shí)結(jié)果就變成了10,0fmt.Println(w.b, w.c)
}
uintptr(unsafe.Pointer(w))獲取了w的指針起始值;unsafe.Offsetof(w.b)獲取b變量的偏移量;- 兩個(gè)相加就得到了
b的地址值,將通用指針Pointer轉(zhuǎn)換成具體指針((*int)(b)),通過(guò)*符號(hào)取值,然后賦值。*((*int)(b))相當(dāng)于把(*int)(b)轉(zhuǎn)換成int了,最后對(duì)變量重新賦值成 10,這樣指針運(yùn)算就完成了。
總結(jié)
以上是生活随笔為你收集整理的Go 学习笔记(74)— Go 标准库之 unsafe的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 2022-2028年中国氟橡胶密封件行业
- 下一篇: 2022-2028年中国遇水膨胀橡胶行业