gopl-zh.github.com/ch13/ch13-02.md

5.5 KiB
Raw Blame History

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)