13.2. unsafe.Pointer

大多數指針類型會寫成*T,表示是“一個指向T類型變量的指針”。unsafe.Pointer是特別定義的一種指針類型(譯註:類似C語言中的void*類型的指針),它可以包含任意類型變量的地址。當然,我們不可以直接通過*p來獲取unsafe.Pointer指針指向的真實變量的值,因為我們並不知道變量的具體類型。和普通指針一樣,unsafe.Pointer指針也是可以比較的,並且支持和nil常量比較判斷是否為空指針。

一個普通的*T類型指針可以被轉化為unsafe.Pointer類型指針,並且一個unsafe.Pointer類型指針也可以被轉回普通的指針,被轉回普通的指針類型並不需要和原始的*T類型相同。通過將*float64類型指針轉化為*uint64類型指針,我們可以查看一個浮點數變量的位模式。

package math

func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }

fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000"

通過轉為新類型指針,我們可以更新浮點數的位模式。通過位模式操作浮點數是可以的,但是更重要的意義是指針轉換語法讓我們可以在不破壞類型系統的前提下向內存寫入任意的值。

一個unsafe.Pointer指針也可以被轉化為uintptr類型,然後保存到指針型數值變量中(譯註:這只是和當前指針相同的一個數字值,並不是一個指針),然後用以做必要的指針數值運算。(第三章內容,uintptr是一個無符號的整型數,足以保存一個地址)這種轉換雖然也是可逆的,但是將uintptr轉為unsafe.Pointer指針可能會破壞類型系統,因為並不是所有的數字都是有效的內存地址。

許多將unsafe.Pointer指針轉為原生數字,然後再轉回為unsafe.Pointer類型指針的操作也是不安全的。比如下面的例子需要將變量x的地址加上b字段地址偏移量轉化為*int16類型指針,然後通過該指針更新x.b:

gopl.io/ch13/unsafeptr

var x struct {
    a bool
    b int16
    c []int
}

// 和 pb := &x.b 等價
pb := (*int16)(unsafe.Pointer(
    uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
fmt.Println(x.b) // "42"

上面的寫法儘管很繁瑣,但在這裡並不是一件壞事,因為這些功能應該很謹慎地使用。不要試圖引入一個uintptr類型的臨時變量,因為它可能會破壞代碼的安全性(譯註:這是真正可以體會unsafe包為何不安全的例子)。下面段代碼是錯誤的:

// NOTE: subtly incorrect!
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42

產生錯誤的原因很微妙。有時候垃圾回收器會移動一些變量以降低內存碎片等問題。這類垃圾回收器被稱為移動GC。當一個變量被移動,所有的保存該變量舊地址的指針必須同時被更新為變量移動後的新地址。從垃圾收集器的視角來看,一個unsafe.Pointer是一個指向變量的指針,因此當變量被移動時對應的指針也必須被更新;但是uintptr類型的臨時變量只是一個普通的數字,所以其值不應該被改變。上面錯誤的代碼因為引入一個非指針的臨時變量tmp,導致垃圾收集器無法正確識別這個是一個指向變量x的指針。當第二個語句執行時,變量x可能已經被轉移,這時候臨時變量tmp也就不再是現在的&x.b地址。第三個向之前無效地址空間的賦值語句將徹底摧毀整個程序!

還有很多類似原因導致的錯誤。例如這條語句:

pT := uintptr(unsafe.Pointer(new(T))) // 提示: 錯誤!

這裡並沒有指針引用new新創建的變量,因此該語句執行完成之後,垃圾收集器有權馬上回收其內存空間,所以返回的pT將是無效的地址。

雖然目前的Go語言實現還沒有使用移動GC(譯註:未來可能實現),但這不該是編寫錯誤代碼僥倖的理由:當前的Go語言實現已經有移動變量的場景。在5.2節我們提到goroutine的棧是根據需要動態增長的。當發生棧動態增長的時候,原來棧中的所有變量可能需要被移動到新的更大的棧中,所以我們並不能確保變量的地址在整個使用週期內是不變的。

在編寫本文時,還沒有清晰的原則來指引Go程序員,什麼樣的unsafe.Pointer和uintptr的轉換是不安全的(參考 Issue7192 ). 譯註: 該問題已經關閉),因此我們強烈建議按照最壞的方式處理。將所有包含變量地址的uintptr類型變量當作BUG處理,同時減少不必要的unsafe.Pointer類型到uintptr類型的轉換。在第一個例子中,有三個轉換——字段偏移量到uintptr的轉換和轉回unsafe.Pointer類型的操作——所有的轉換全在一個表達式完成。

當調用一個庫函數,並且返回的是uintptr類型地址時(譯註:普通方法實現的函數儘量不要返回該類型。下面例子是reflect包的函數,reflect包和unsafe包一樣都是採用特殊技術實現的,編譯器可能給它們開了後門),比如下面反射包中的相關函數,返回的結果應該立即轉換為unsafe.Pointer以確保指針指向的是相同的變量。

package reflect

func (Value) Pointer() uintptr
func (Value) UnsafeAddr() uintptr
func (Value) InterfaceData() [2]uintptr // (index 1)