帶你深度瞭解 unsafe-Pointer
一、前言
相信看過 Go 源碼的同學已經對 unsafe.Pointer 非常的眼熟,因爲這個類型可以說在源碼中是隨處可見:map、channel、interface、slice… 但凡你能想到的內容,基本都會有 unsafe.Pointer 的影子。
看字面意思,unsafe.Pointer 是 “不安全的指針”,指針就指針吧,還安不安全的是個什麼鬼?
二、指針
爲了更深入瞭解 unsafe.Pointer, 我們首先講解一下指針類型。
2.1 指針的運用
爲什麼需要指針類型呢?參考文獻 go101.org 裏舉了這樣一個例子:
package main
import "fmt"
func double(x int) {
x += x
}
func main() {
var a = 3
double(a)
fmt.Println(a) // 3
}
我想在 double 函數里將 a 翻倍,但是例子中的函數卻做不到。爲什麼?因爲 Go 語言的函數傳參都是 值傳遞。double 函數里的 x 只是實參 a 的一個拷貝,在函數內部對 x 的操作不能反饋到實參 a。
如果這時,有一個指針就可以解決問題了!這也是我們常用的 “伎倆”。
package main
import "fmt"
func double(x *int) {
*x += *x
}
func main() {
var a = 3
double(&a)
fmt.Println(a) // 6
p := &a
double(p)
fmt.Println(a, p == nil) // 12 false
}
說明:
- *x += *x,這一句把 x 指向的值(也就是 &a 指向的值,即變量 a)變爲原來的 2 倍。但是對 x 本身(一個指針)的操作卻不會影響外層的 a,所以 x=nil 掀不起任何大風大浪。
2.2 指針的限制
然而,相比於 C 語言中指針的靈活,Go 的指針多了一些限制。但這也算是 Go 的成功之處:既可以享受指針帶來的便利,又避免了指針的危險性,限制性如下:
-
Go 中指針不能進行算術運算。例如:&a++
-
Go 中不同類型的指針不能相互轉換。例如:var a int = 1;f := (*float64)(&a)
-
Go 中不同類型的指針不能比較和相互賦值,例如:var a int = 1;var f float64;f = &a;&a == &f
以上指針錯誤的使用方式在 Go 中都會編譯報錯。
三、unsafe.Pointer 是什麼
unsafe.Pointer 可以指向任意類型的指針。不能進行指針運算,不能讀取內存存儲的值 (想讀取的話需要轉成相對應類型的指針)。它是橋樑,讓任意類型的指針實現相互轉換, 也可以轉換成 uintptr 進行指針運算。
Pointer 是在 unsafe 的 package 裏。源代碼中的比較多,摘取部分如下:
type Pointer *ArbitraryType
Pointer represents a pointer to an arbitrary type. There are four special operations
available for type Pointer that are not available for other types: // Pointer代表了一個任意類型的指針。Pointer類型有四種特殊的操作是其他類型不能使用的:
- A pointer value of any type can be converted to a Pointer. // 任意類型的指針可以被轉換爲Pointer
- A Pointer can be converted to a pointer value of any type. // Pointer可以被轉換爲任務類型的值的指針
- A uintptr can be converted to a Pointer. // uintptr可以被轉換爲Pointer
- A Pointer can be converted to a uintptr. // Pointer可以被轉換爲uintptr
Pointer therefore allows a program to defeat the type system and read and write
arbitrary memory. It should be used with extreme care. // 因此Pointer允許程序不按類型系統的要求來讀寫任意的內存,應該非常小心地使用它。
一般的指針運算有三個步驟。
-
將 unsafe.Pointer 轉換爲 uintptr
-
對 uintptr 執行算術運算
3. 將 uintptr 轉換回 unsafe.Pointer, 然後轉成訪問指向的對象的指針類型。
3.1 什麼是 uintptr
源碼地址:src/builtin/builtin.go:
// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr
uintptr 是一個整數類型,足夠大能保存任何一種指針類型。uintptr 指的是具體的內存地址,不是個指針,因此 uintptr 地址關聯的對象可以被垃圾回收。而 unsafe.Pointer 有指針語義,可以保護它不會被垃圾回收。
指針類型、unsafe.Pointer、uintptr 三者關係如下:
指針類型 *T <-> unsafe.Pointer <-> uintptr
uintptr 是可用於存儲指針的整型,而整型是可以進行數學運算的。因此,將 unsafe.Pointer 轉化爲 uintptr 類型後,就可以讓本不具備運算能力的指針具備了指針運算能力。
3.2 總結
-
unsafe.Pointer 可以和任意的指針類型進行轉換,意味着可以藉助 unsafe.Pointer 完成不同指針類型之間的轉換。
-
unsafe.Pointer 可以轉換爲 uintptr,而 uintptr 擁有計算能力,因此指針可以藉助 unsafe.Pointer 和 uintptr 完成算術運算,進而直接操作內存。
四、unsafe.Pointer 實戰
4.1 指針類型轉換
unsafe.Pointer 可以在不同的指針類型之間做轉化,從而可以表示任意可尋址的指針類型,利用 unsafe.Pointer 爲中介,即可完成指針類型的轉換。先舉一個簡單例子,看一下不同指針類型之間的轉換過程:
package main
import (
"fmt"
"unsafe"
)
func main() {
i := 100
intI := &i
var floatI *float64
floatI = (*float64)(unsafe.Pointer(intI))
*floatI = *floatI * 3
fmt.Printf("%T\n", i)
fmt.Println(i)
fmt.Printf("%T\n", intI)
fmt.Printf("%T\n", floatI)
}
// 輸出
int
300
*int
*float64
該例子中定義了兩個指針變量分別是 *int 類型的 intI 和 *float64 類型的 floatI,然後先對 intI 做了類型 unsafe.Pointer 的轉換,隨後進行 *float64 類型的轉換;然後對 *float64 進行乘法操作,最終影響到了 i 變量,也從側面證明了 *float64 的指針變量是指向 i 變量的內存地址的。
然後我們看一個類型轉換經典的例子:實現 string 和 slice 之間的轉換,要求是零拷貝。如果按照以往的方式,循環遍歷,然後挨個拷貝賦值是無法完成目標的,這個時候只能考慮共享底層 []byte 數組纔可以實現零拷貝轉換。string 和 []byte 在運行時的類型表示爲 reflect.StringHeader 和 reflect.SliceHeader
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
上面是反射包下的結構體,路徑:src/reflect/value.go。只需要共享底層 []byte 數組就可以實現 zero-copy。
使用 unsafe.Pointer 將 string 或 []byte 轉換爲 *reflect.StringHeader 或 *reflect.SliceHeader,然後通過構造方式,完成底層 []byte 數組的共享,最後通過指針類型轉換方式再次轉換回來,代碼如下:
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))
}
4.2 指針運算
案例一:通過指針運算,修改數組內部的值。
package main
import (
"fmt"
"unsafe"
)
func main() {
arr := [3]int{1, 2, 3}
ap := &arr
arr0p := (*int)(unsafe.Pointer(ap))
arr1p := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(ap)) + unsafe.Sizeof(arr[0])))
*arr0p += 10
*arr1p += 20
fmt.Println(arr) // [11 22 3]
}
案例二:通過指針運算,修改結構體內的值。
package main
import (
"fmt"
"unsafe"
)
type user struct {
name string
age int
}
func main() {
u := new(user)
fmt.Println(*u) // { 0}
pName := (*string)(unsafe.Pointer(u))
*pName = "張三"
pAge := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(u)) + unsafe.Offsetof(u.age)))
*pAge = 20
fmt.Println(*u) // {張三 20}
}
通過以上案例,unsafe.Pointer 繞過了 Go 的類型系統,達到直接操作內存的目的,使用它有一定的風險性。具體使用場景如下:
-
作爲不同類型指針互相轉換的中介;
-
利用 uintptr 突破指針不能進行算術運算的限制,從而達到直接操作內存的目的。
但是在某些場景下,使用 unsafe 包提供的函數會提升代碼的效率,Go 源碼中也是大量使用 unsafe 包。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ruIWFC8e8U2bUsSdD5jJLA