一篇文章把 Go指針的扒得干干凈凈
除了常規(guī)的指針外,Go 語言在 unsafe 包里其實還通過 unsafe.Pointer 提供了通用指針,通過這個通用指針以及 unsafe 包的其他幾個功能又讓使用者能夠繞過 Go 語言的類型系統(tǒng)直接操作內(nèi)存進(jìn)行例如:指針類型轉(zhuǎn)換,讀寫結(jié)構(gòu)體私有成員這樣操作。
網(wǎng)管覺得正是因為功能強(qiáng)大同時伴隨著操作不慎讀寫了錯誤的內(nèi)存地址即會造成的嚴(yán)重后果所以 Go 語言的設(shè)計者才會把這些功能放在 unsafe 包里。其實也沒有想得那么不安全,掌握好了使用得當(dāng)還是能帶來很大的便利的,在一些偏向底層的源碼中 unsafe 包使用的頻率還是不低的。
對于勵志成為高階 Gopher 的各位,這也是一項必不可少需要掌握的技能啦。接下來網(wǎng)管就帶大家從基本的指針使用方法和限制開始看看怎么用 unsafe 包跨過這些限制直接讀寫內(nèi)存。
# 基礎(chǔ)知識
指針保存著一個值的內(nèi)存地址,類型 *T代表指向T 類型值的指針。其零值為nil。
&操作符為它的操作數(shù)生成一個指針。
i := 42
p = &i
*操作符則會取出指針指向地址的值,這個操作也叫做“解引用”。
fmt.Println(*p) // 通過指針p讀取存儲的值
*p = 21 // 通過指針p設(shè)置p執(zhí)行的內(nèi)存地址存儲的值
為什么需要指針類型呢?參考一個從go101網(wǎng)站上看到的例子 :
package main
import "fmt"
func double(x int) {
x += x
}
func main() {
var a = 3
double(a)
fmt.Println(a) // 3
}
在 double 函數(shù)里將 a 翻倍,但是例子中的函數(shù)卻做不到。因為 Go 語言的函數(shù)傳參都是值傳遞。double 函數(shù)里的 x 只是實參 a 的一個拷貝,在函數(shù)內(nèi)部對 x 的操作不能反饋到實參 a。
把參數(shù)換成一個指針就可以解決這個問題了。
package main
import "fmt"
func double(x *int) {
*x += *x
x = nil
}
func main() {
var a = 3
double(&a)
fmt.Println(a) // 6
p := &a
double(p)
fmt.Println(a, p == nil) // 12 false
}
上面的程序乍一看你可能對下面這一行代碼有些疑惑
x = nil
稍微思考一下上面說的Go語言里面參數(shù)都是值傳遞,你就會知道這一行代碼根本不影響外面的變量 a。因為參數(shù)都是值傳遞,所以函數(shù)內(nèi)的 x 也只是對 &a 的一個拷貝。
*x += *x
這一句把 x 指向的值(也就是 &a 指向的值,即變量 a)變?yōu)樵瓉淼?2 倍。但是對 x 本身(一個指針)的操作卻不會影響外層的 a,所以在double函數(shù)內(nèi)部的 x=nil 不會影響外面。
# 指針的限制
相較于 C 語言指針的靈活,Go 語言里指針多了不少限制,不過這讓我們:既可以享受指針帶來的便利,又避免了指針的危險性。下面就簡單說一下 Go 對指針操作的一些限制
限制一:指針不能參與運算
來看一個簡單的例子:
package main
import "fmt"
func main() {
a := 5
p := a
fmt.Println(p)
p = &a + 3
}
上面的代碼將不能通過編譯,會報編譯錯誤:
invalid operation: &a + 3 (mismatched types *int and int)
也就是說 Go 不允許對指針進(jìn)行數(shù)學(xué)運算。
限制二:不同類型的指針不允許相互轉(zhuǎn)換。
下面的程序同樣也不能編譯成功:
package main
func main() {
var a int = 100
var f *float64
f = *float64(&a)
}
限制三:不同類型的指針不能比較和相互賦值
這條限制同上面的限制二,因為指針之間不能做類型轉(zhuǎn)換,所以也沒法使用==或者!=進(jìn)行比較了,同樣不同類型的指針變量相互之間不能賦值。比如下面這樣,也是會報編譯錯誤。
package main
func main() {
var a int = 100
var f *float64
f = &a
}
Go語言的指針是類型安全的,但它有很多限制,所以 Go 還有提供了可以進(jìn)行類型轉(zhuǎn)換的通用指針,這就是 unsafe 包提供的 unsafe.Pointer。在某些情況下,它會使代碼更高效,當(dāng)然如果掌握不好就使用,也更容易讓程序崩潰。
# unsafe 包
unsafe 包用于編譯階段可以繞過 Go 語言的類型系統(tǒng),直接操作內(nèi)存。例如,利用 unsafe 包操作一個結(jié)構(gòu)體的未導(dǎo)出成員。unsafe 包讓我可以直接讀寫內(nèi)存的能力。
unsafe包只有兩個類型,三個函數(shù),但是功能很強(qiáng)大。
type ArbitraryType int
type Pointer *ArbitraryType
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr
ArbitraryType是int的一個別名,在 Go 中ArbitraryType有特殊的意義。代表一個任意Go表達(dá)式類型。Pointer是int指針類型的一個別名,在 Go 中可以把任意指針類型轉(zhuǎn)換成unsafe.Pointer類型。
三個函數(shù)的參數(shù)均是ArbitraryType類型,就是接受任何類型的變量。
Sizeof接受任意類型的值(表達(dá)式),返回其占用的字節(jié)數(shù),這和c語言里面不同,c語言里面sizeof函數(shù)的參數(shù)是類型,而這里是一個值,比如一個變量。Offsetof:返回結(jié)構(gòu)體成員在內(nèi)存中的位置距離結(jié)構(gòu)體起始處的字節(jié)數(shù),所傳參數(shù)必須是結(jié)構(gòu)體的成員(結(jié)構(gòu)體指針指向的地址就是結(jié)構(gòu)體起始處的地址,即第一個成員的內(nèi)存地址)。Alignof返回變量對齊字節(jié)數(shù)量,這個函數(shù)雖然接收的是任何類型的變量,但是有一個前提,就是變量要是一個struct類型,且還不能直接將這個struct類型的變量當(dāng)作參數(shù),只能將這個struct類型變量的值當(dāng)作參數(shù),具體細(xì)節(jié)咱們到以后聊內(nèi)存對齊的文章里再說。
注意以上三個函數(shù)返回的結(jié)果都是 uintptr 類型,這和 unsafe.Pointer 可以相互轉(zhuǎn)換。三個函數(shù)都是在編譯期間執(zhí)行
unsafe.Pointer
unsafe.Pointer稱為通用指針,官方文檔對該類型有四個重要描述:
任何類型的指針都可以被轉(zhuǎn)化為Pointer
Pointer可以被轉(zhuǎn)化為任何類型的指針
uintptr可以被轉(zhuǎn)化為Pointer
Pointer可以被轉(zhuǎn)化為uintptr
unsafe.Pointer是特別定義的一種指針類型(譯注:類似C語言中的void類型的指針),在Go 語言中是用于各種指針相互轉(zhuǎn)換的橋梁,它可以持有任意類型變量的地址。
什么叫"可以持有任意類型變量的地址"呢?意思就是使用 unsafe.Pointer 轉(zhuǎn)換的變量,該變量一定要是指針類型,否則編譯會報錯。
a := 1
b := unsafe.Pointer(a) //報錯
b := unsafe.Pointer(&a) // 正確
和普通指針一樣,unsafe.Pointer 指針也是可以比較的,并且支持和 nil 比較判斷是否為空指針。
unsafe.Pointer 不能直接進(jìn)行數(shù)學(xué)運算,但可以把它轉(zhuǎn)換成 uintptr,對 uintptr 類型進(jìn)行數(shù)學(xué)運算,再轉(zhuǎn)換成 unsafe.Pointer 類型。
// uintptr、unsafe.Pointer和普通指針之間的轉(zhuǎn)換關(guān)系
uintptr <==> unsafe.Pointer <==> *T
uintptr
uintptr是 Go 語言的內(nèi)置類型,是能存儲指針的整型,在64位平臺上底層的數(shù)據(jù)類型是 uint64。
// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr
typedef unsigned long long int uint64;
typedef uint64 uintptr;
一個unsafe.Pointer指針也可以被轉(zhuǎn)化為uintptr類型,然后保存到uintptr類型的變量中(注:這個變量只是和當(dāng)前指針有相同的一個數(shù)字值,并不是一個指針),然后用以做必要的指針數(shù)值運算。(uintptr是一個無符號的整型數(shù),足以保存一個地址)這種轉(zhuǎn)換雖然也是可逆的,但是隨便將一個 uintptr 轉(zhuǎn)為 unsafe.Pointer指針可能會破壞類型系統(tǒng),因為并不是所有的數(shù)字都是有效的內(nèi)存地址。
還有一點要注意的是,uintptr 并沒有指針的語義,意思就是存儲 uintptr 值的內(nèi)存地址在Go發(fā)生GC時會被回收。而 unsafe.Pointer 有指針語義,可以保護(hù)它不會被垃圾回收。
聊了這么多概念性的話題,接下來網(wǎng)管帶大家一起看看怎么使用 unsafe.Pointer 進(jìn)行指針轉(zhuǎn)換以及結(jié)合 uintptr 讀寫結(jié)構(gòu)體的私有成員。
# 應(yīng)用示例
使用unsafe.Pointer進(jìn)行指針類型轉(zhuǎn)換
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
v1 := uint(12)
v2 := int(13)
fmt.Println(reflect.TypeOf(v1)) //uint
fmt.Println(reflect.TypeOf(v2)) //int
fmt.Println(reflect.TypeOf(&v1)) //*uint
fmt.Println(reflect.TypeOf(&v2)) //*int
p := &v1
p = (*uint)(unsafe.Pointer(&v2)) //使用unsafe.Pointer進(jìn)行類型的轉(zhuǎn)換
fmt.Println(reflect.TypeOf(p)) // *unit
fmt.Println(*p) //13
}
使用unsafe.Pointer 讀寫結(jié)構(gòu)體的私有成員
通過 Offsetof 方法可以獲取結(jié)構(gòu)體成員的偏移量,進(jìn)而獲取成員的地址,讀寫該地址的內(nèi)存,就可以達(dá)到改變成員值的目的。
這里有一個內(nèi)存分配相關(guān)的事實:結(jié)構(gòu)體會被分配一塊連續(xù)的內(nèi)存,結(jié)構(gòu)體的地址也代表了第一個成員的地址。
package main
import (
"fmt"
"unsafe"
)
func main() {
var x struct {
a int
b int
c []int
}
// unsafe.Offsetof 函數(shù)的參數(shù)必須是一個字段, 比如 x.b, 方法會返回 b 字段相對于 x 起始地址的偏移量, 包括可能的空洞。
// 指針運算 uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)。
// 和 pb := &x.b 等價
pb := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
fmt.Println(x.b) // "42"
}
上面的寫法盡管很繁瑣,但在這里并不是一件壞事,因為這些功能應(yīng)該很謹(jǐn)慎地使用。不要試圖引入一個uintptr類型的臨時變量,因為它可能會破壞代碼的安全性
如果改為下面這種用法是有風(fēng)險的:
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42
隨著程序執(zhí)行的進(jìn)行,goroutine 會經(jīng)常發(fā)生棧擴(kuò)容或者??s容,會把舊棧內(nèi)存的數(shù)據(jù)拷貝到新棧區(qū)然后更改所有指針的指向。一個 unsafe.Pointer 是一個指針,因此當(dāng)它指向的數(shù)據(jù)被移動到新棧區(qū)后指針也會被更新。但是uintptr 類型的臨時變量只是一個普通的數(shù)字,所以其值不會該被改變。上面錯誤的代碼因為引入一個非指針的臨時變量 tmp,導(dǎo)致系統(tǒng)無法正確識別這個是一個指向變量 x 的指針。當(dāng)?shù)诙€語句執(zhí)行時,變量 x 的數(shù)據(jù)可能已經(jīng)被轉(zhuǎn)移,這時候臨時變量tmp也就不再是現(xiàn)在的 &x.b 的地址。第三個語句向之前無效地址空間的賦值語句將讓整個程序崩潰。
string 和 []byte 零拷貝轉(zhuǎn)換
這是一個非常經(jīng)典的例子。實現(xiàn)字符串和 bytes 切片之間的零拷貝轉(zhuǎn)換。
string和[]byte 在運行時的類型表示為reflect.StringHeader和reflect.SliceHeader
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
type StringHeader struct {
Data uintptr
Len int
}
只需要共享底層 []byte 數(shù)組就可以實現(xiàn)零拷貝轉(zhuǎn)換。
代碼比較簡單,不作詳細(xì)解釋。通過構(gòu)造reflect.StringHeader和reflect.SliceHeader,來完成string和 []byte 之間的轉(zhuǎn)換。
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
s := "Hello World"
b := string2bytes(s)
fmt.Println(b)
s = bytes2string(b)
fmt.Println(s)
}
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))
}
func bytes2string(b []byte) string {
sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := reflect.StringHeader{
Data: sliceHeader.Data,
Len: sliceHeader.Len,
}
return *(*string)(unsafe.Pointer(&sh))
}
# 總結(jié)
Go 的源碼中也在大量使用 unsafe 包,通過 unsafe 包繞過 Go 指針的限制,達(dá)到直接操作內(nèi)存的目的,使用它有一定的風(fēng)險性,但是在一些場景下,可以提升代碼的效率。
參考資料
unsafe.Pointer 和 uintptr :https://www.cnblogs.com/echojson/p/10743530.html

???
