帶你深度瞭解 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
}

說明:

  1. *x += *x,這一句把 x 指向的值(也就是 &a 指向的值,即變量 a)變爲原來的 2 倍。但是對 x 本身(一個指針)的操作卻不會影響外層的 a,所以 x=nil 掀不起任何大風大浪。

2.2 指針的限制

然而,相比於 C 語言中指針的靈活,Go 的指針多了一些限制。但這也算是 Go 的成功之處:既可以享受指針帶來的便利,又避免了指針的危險性,限制性如下:

  1. Go 中指針不能進行算術運算。例如:&a++

  2. Go 中不同類型的指針不能相互轉換。例如:var a int = 1;f := (*float64)(&a)

  3. 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允許程序不按類型系統的要求來讀寫任意的內存,應該非常小心地使用它。

一般的指針運算有三個步驟。

  1. 將 unsafe.Pointer 轉換爲 uintptr 

  2. 對 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 總結

  1. unsafe.Pointer 可以和任意的指針類型進行轉換,意味着可以藉助 unsafe.Pointer 完成不同指針類型之間的轉換。

  2. 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 的類型系統,達到直接操作內存的目的,使用它有一定的風險性。具體使用場景如下:

  1. 作爲不同類型指針互相轉換的中介;

  2. 利用 uintptr 突破指針不能進行算術運算的限制,從而達到直接操作內存的目的。

但是在某些場景下,使用 unsafe 包提供的函數會提升代碼的效率,Go 源碼中也是大量使用 unsafe 包。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ruIWFC8e8U2bUsSdD5jJLA