gopl-zh.github.com/ch13/ch13-02.md
2015-12-18 10:53:03 +08:00

67 lines
4.6 KiB
Markdown

## 13.2. unsafe.Pointer
大多數指鍼類型寫成 *T, 含義是 "一個指向T類型變量的指鍼". `unsafe.Pointer` 是特別定義的一種指鍼類型, 它可以包含任意類型變量的地址. 當然, 我們不可以直接使用 *p 獲取 `unsafe.Pointer` 指鍼指向的眞實變量, 因爲我們並不知道變量的類型. 和普通指鍼一樣, `unsafe.Pointer` 指鍼是可以比較的, 支持和 nil 比較判斷是否爲空指鍼.
一個普通的 *T 類型指鍼可以被轉化爲 `unsafe.Pointer` 類型指鍼, 並且一個 `unsafe.Pointer` 類型指鍼也可以被轉迴普通指鍼, 也可以是和 *T 不同類型的指鍼. 通過將 `*float64` 類型指鍼 轉化爲 `*uint64` 類型指鍼, 我們可以檢査一個浮點數變量的位模式.
```Go
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`:
```Go
//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`. 第三個賦值語句將徹底摧毀那個之前的那部分內存空間.
有很多類似原因導緻的錯誤. 例如這條語句:
```Go
pT := uintptr(unsafe.Pointer(new(T))) // 提示: 錯誤!
```
這裡並沒有指鍼引用 `new` 新創建的變量, 因此語句執行完成之後, 垃圾收集器有權迴收其內存空間, 所以返迴的 `pT` 保存將是無效的地址.
目前的Go語言實現還沒有使用移動GC(未來可能實現), 但這不該是僥倖的理由: 當前的Go實現已經有移動變量的場景. 在5.2節我們提到goroutine的棧是根據需要動態增長的. 當這個時候, 原來棧中的所以變量可能需要被移動到新的更大的棧中, 所以我們無法確保變量的地址在整個使用週期內保持不變.
在編寫本文時, 還沒有清晰的原則就指引Go程序員, 什麽樣 `unsafe.Pointer``uintptr` 的轉換是不安全的(參考 [Go issue7192](https://github.com/golang/go/issues/7192). 譯註: 該問題已經脩復.), 因此我們強烈建議按照最壞的方式處理. 將所有包含變量 `y` 地址的 `uintptr` 類型變量當作 BUG 處理, 同時減少不必要的 `unsafe.Pointer``uintptr` 的轉換. 在第一個例子中, 有三個到 `uintptr` 的轉換, 字段偏移量的運算, 所有的轉換全在一個表達式完成.
當調用一個庫函數, 並且返迴的是 `uintptr` 類型是, 比如下面反射包中的相關函數,
返迴的結果應該立卽轉換爲 `unsafe.Pointer` 以確保指鍼指向的是相同的變量.
```Go
package reflect
func (Value) Pointer() uintptr
func (Value) UnsafeAddr() uintptr
func (Value) InterfaceData() [2]uintptr // (index 1)
```