## 13.2. unsafe.Pointer
大多數指針類型會寫成`*T`,表示是“一個指向T類型變量的指針”。unsafe.Pointer是特別定義的一種指針類型(譯注:類似C語言中的`void*`類型的指針),它可以包含任意類型變量的地址。當然,我們不可以直接通過`*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:
<u><i>gopl.io/ch13/unsafeptr</i></u>
```Go
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包為何不安全的例子)。下面段代碼是錯誤的:
```Go
// 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`地址。第三個向之前無效地址空間的賦值語句將徹底摧毀整個程序!
還有很多類似原因導致的錯誤。例如這條語句:
```Go
pT := uintptr(unsafe.Pointer(new(T))) // 提示: 錯誤!
```
這里并沒有指針引用`new`新創建的變量,因此該語句執行完成之后,垃圾收集器有權馬上回收其內存空間,所以返回的pT將是無效的地址。
雖然目前的Go語言實現還沒有使用移動GC(譯注:未來可能實現),但這不該是編寫錯誤代碼僥幸的理由:當前的Go語言實現已經有移動變量的場景。在5.2節我們提到goroutine的棧是根據需要動態增長的。當發生棧動態增長的時候,原來棧中的所有變量可能需要被移動到新的更大的棧中,所以我們并不能確保變量的地址在整個使用周期內是不變的。
在編寫本文時,還沒有清晰的原則來指引Go程序員,什么樣的unsafe.Pointer和uintptr的轉換是不安全的(參考 [Issue7192](https://github.com/golang/go/issues/7192) ). 譯注: 該問題已經關閉),因此我們強烈建議按照最壞的方式處理。將所有包含變量地址的uintptr類型變量當作BUG處理,同時減少不必要的unsafe.Pointer類型到uintptr類型的轉換。在第一個例子中,有三個轉換——字段偏移量到uintptr的轉換和轉回unsafe.Pointer類型的操作——所有的轉換全在一個表達式完成。
當調用一個庫函數,并且返回的是uintptr類型地址時(譯注:普通方法實現的函數盡量不要返回該類型。下面例子是reflect包的函數,reflect包和unsafe包一樣都是采用特殊技術實現的,編譯器可能給它們開了后門),比如下面反射包中的相關函數,返回的結果應該立即轉換為unsafe.Pointer以確保指針指向的是相同的變量。
```Go
package reflect
func (Value) Pointer() uintptr
func (Value) UnsafeAddr() uintptr
func (Value) InterfaceData() [2]uintptr // (index 1)
```
- 前言
- Go語言起源
- Go語言項目
- 本書的組織
- 更多的信息
- 致謝
- 入門
- Hello, World
- 命令行參數
- 查找重復的行
- GIF動畫
- 獲取URL
- 并發獲取多個URL
- Web服務
- 本章要點
- 程序結構
- 命名
- 聲明
- 變量
- 賦值
- 類型
- 包和文件
- 作用域
- 基礎數據類型
- 整型
- 浮點數
- 復數
- 布爾型
- 字符串
- 常量
- 復合數據類型
- 數組
- Slice
- Map
- 結構體
- JSON
- 文本和HTML模板
- 函數
- 函數聲明
- 遞歸
- 多返回值
- 錯誤
- 函數值
- 匿名函數
- 可變參數
- Deferred函數
- Panic異常
- Recover捕獲異常
- 方法
- 方法聲明
- 基于指針對象的方法
- 通過嵌入結構體來擴展類型
- 方法值和方法表達式
- 示例: Bit數組
- 封裝
- 接口
- 接口是合約
- 接口類型
- 實現接口的條件
- flag.Value接口
- 接口值
- sort.Interface接口
- http.Handler接口
- error接口
- 示例: 表達式求值
- 類型斷言
- 基于類型斷言識別錯誤類型
- 通過類型斷言查詢接口
- 類型分支
- 示例: 基于標記的XML解碼
- 補充幾點
- Goroutines和Channels
- Goroutines
- 示例: 并發的Clock服務
- 示例: 并發的Echo服務
- Channels
- 并發的循環
- 示例: 并發的Web爬蟲
- 基于select的多路復用
- 并發的退出
- 示例: 聊天服務
- 基于共享變量的并發
- 競爭條件
- sync.Mutex互斥鎖
- sync.RWMutex讀寫鎖
- 內存同步
- 競爭條件檢測
- 示例: 并發的非阻塞緩存
- Goroutines和線程
- 包和工具
- 包簡介
- 導入路徑
- 包聲明
- 導入聲明
- 包的匿名導入
- 包和命名
- 工具
- 測試
- go test
- 測試函數
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
- 反射
- 為何需要反射?
- reflect.Type和reflect.Value
- Display遞歸打印
- 示例: 編碼S表達式
- 通過reflect.Value修改值
- 示例: 解碼S表達式
- 顯示一個類型的方法集
- 幾點忠告
- 底層編程
- unsafe.Sizeof, Alignof 和 Offsetof
- unsafe.Pointer
- 示例: 深度相等判斷
- 通過cgo調用C代碼
- 幾點忠告
- 附錄
- 附錄A:原文勘誤
- 附錄B:作者譯者
- 附錄C:譯文授權
- 附錄D:其它語言