Skip to content

Latest commit

 

History

History
70 lines (46 loc) · 5.5 KB

ch13-02.md

File metadata and controls

70 lines (46 loc) · 5.5 KB

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)