Go unsafe 包探究

nove001發表於2019-05-16

unsafe 包的作用有兩個:

  • 實現任意不同型別指標之間的轉換;
  • 實現指標運算(偏移)操作;

包介面比較簡單,包括 3 個函式:

  • func Alignof(x ArbitraryType) uintptr 變數對齊;
  • func Offsetof(x ArbitraryType) uintptr 計算結構體(struct)內屬性值的偏移位元組大小,即相對結構體起始地址的大小;
  • func Sizeof(x ArbitraryType) uintptr 型別變數自身佔用位元組大小,注意,不包括變數引用的地址;

unsafe 函式都是在編譯時計算返回結果的,所以,可以直接用於常量賦值,也要注意,儘量不要將執行時變數型別(例如 slice)作為這些函式的引數出入,可能會導致非預期的結果。

包括 2 種型別:

  • type ArbitraryType 佔位符,實際上表示任意的變數型別;
  • type Pointer 指向任意型別的指標型別。類似 C 中 void * 型別。

unsafe.Pointer 是本包的精華,也是被使用最多的功能點。Pointer 允許程式(開發者)跳脫 Go 的型別系統,(通過指標轉換與運算)讀寫任意記憶體,所以要小心使用。 Pointer 總結下來就兩個特性,也是實現前面說的目標(作用)的基礎:

  • Pointer 能與任意型別的指標值互相轉換;
  • Pointer 能與 uintpter 相互轉換;

uintptr 是無符號整型,被用來存放指標值(地址)。unsafe.Pointer + uintptr 就能實現指標偏移計算了。因為 uintptr 變數存放的是某個變數的地址,因此,uintpter 變數值對應的記憶體地址塊(對應的變數)可能會被 GC 回收掉。unsafe.Pointer 本質上就是指標,該型別變數指向的記憶體塊則不會被回收,因此,應該使用 unsafe.Pointer 型別變數來保持變數地址不被回收。

// 不安全的使用

z := uintptr(unsafe.Pointer(&xx))
//todo ...
fmt.Println(z)

//正確使用

sp:=safe.Pointer(&xx)
z = uintptr(sp)
//todo ...
fmt.Println(z)

安全的使用場景

前面說過,使用 Pointer 必須要非常小心才行,官方定義了 6 種安全有效的使用場景。使用 go vet 工具可以檢測出不符合這些場景的呼叫。

  • 將指標 *T1 轉化成 *T2

如果 T2 大於 T1 變數型別的記憶體佔用,並且兩者共享等效的記憶體佈局,則該轉換允許將一種型別資料解釋成為另一種型別。例如:int64 與 float64。

  • 將 Pointer 轉成 uintptr,但是,不能轉回到 Pointer

與 Pointer 不同,uintptr 儲存的地址指向的變數是可以被 GC 回收的。

可以使用 runtime.KeepAlive 函式避免變數被 GC。

  • 將 Pointer 轉成 uintptr,用於偏移運算,將計算結果轉回成 Pointer

通常用於訪問結構體或者陣列等:

// equivalent to f := unsafe.Pointer(&s.f)
f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f))

// equivalent to e := unsafe.Pointer(&x[i])
e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + i*unsafe.Sizeof(x[0]))

特別注意:因為上面說的原因,uintptr 不能放在臨時變數內,所以下面這樣分開使用也是無效的

u := uintptr(p)
p = unsafe.Pointer(u + offset)

另外,做地址偏移的時候,要注意越界的問題,比如:

a = []int{0,1,2,3}
p := unsafe.Pointer(uintptr(unsafe.Pointer(&a)) + len(a) * unsafe.Sizeif(a[0]))

此時,變數 p 指向的地址是未知的,可能會出現不宣告,直接偷偷的讀寫未知記憶體地址的情況,對系統執行穩定性影響很大。

  • 使用函式 syscall.Syscall 時,將 Pointer 值轉成 uintptr

syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))

  • 將函式 reflect.Value.Pointer 或 reflect.Value.UnsafeAddr 返回值,從 uintptr 轉成 Pointer,最終轉成具體的型別值
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))
  • 將 reflect.SliceHeader 或 reflect.StringHeader 的資料欄位(Data)轉成 Pointer,或者從 Pointer 轉成 Data 欄位

Data 欄位返回的也是 uintptr:

var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
hdr.Data = uintptr(unsafe.Pointer(p))              // case 6 (this case)
hdr.Len = n

reflect.SliceHeader 的使用:

package main

import "fmt"
import "unsafe"
import "reflect"
import "runtime"

func main() {
        bs := []byte("Golang")
        var pa *[2]byte // an array pointer
        hdr := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
        pa = (*[2]byte)(unsafe.Pointer(hdr.Data))
        runtime.KeepAlive(&bs)
        fmt.Printf("%s\n", pa) // &Go
        pa[1] = 'a'
        fmt.Printf("%s\n", bs) // Galang
}

如果最後一行的 Printf 不存在的話,runtime.KeepAlive 的呼叫是必須的。 另外,最好不要像下面這樣,直接從 StringHeader 或 StringHeader 直接建立物件:

// Assume p points to a sequence of byte and
// n is the number of bytes in the sequence.
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(new([5]byte)))
// Now the just allocated byte array has lose all
// references and it can be garbage collected now.
hdr.Len = 5
s := *(*string)(unsafe.Pointer(&hdr))

小結

在有些場景下,使用 unsafe.Poniter 可以幫助我們寫出高效的程式碼,例如在 sync/atomic 包內的使用。而且一些底層或 C 呼叫,必須要用到 Poniter。

unsafe 包用於有經驗的開發者繞過 Go 型別系統的安全性限制,一定要深入理解上面的六種使用場景,謹慎使用,否則很容易引起嚴重的記憶體問題,有經驗的開發者都知道,這類問題通常是很難定位的,對系統的穩定性影響很大。

參考

Go 101 : https://go101.org/article/unsafe.html

更多技術文章分享

相關文章