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

4.6 KiB

13.2. unsafe.Pointer

大多數指針類型寫成 *T, 含義是 "一個指向T類型變量的指針". unsafe.Pointer 是特别定義的一種指針類型, 它可以包含任意類型變量的地址. 當然, 我們不可以直接使用 *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 臨時變量. 下面段代碼是不正確的:

錯誤的原因很微妙. 有時候垃圾迴收器會移動一些變量以降低內存碎片的問題.這類垃圾迴收器被稱爲移動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.Pointeruintptr 的轉換是不安全的(參考 Go issue7192. 譯註: 該問題已經脩複.), 因此我們強烈建議按照最壞的方式處理. 將所有包含變量 y 地址的 uintptr 類型變量當作 BUG 處理, 同時減少不必要的 unsafe.Pointeruintptr 的轉換. 在第一個例子中, 有三個到 uintptr 的轉換, 字段偏移量的運算, 所有的轉換全在一個表達式完成.

當調用一個庫函數, 併且返迴的是 uintptr 類型是, 比如下面反射包中的相關函數, 返迴的結果應該立卽轉換爲 unsafe.Pointer 以確保指針指向的是相同的變量.

package reflect

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