golang中的unsafe詳解
一、unsafe 作用
從golang的定義來看,unsafe 是型別安全的操作。顧名思義,它應該非常謹慎地使用; unsafe可能很危險,但也可能非常有用。例如,當使用系統呼叫和Go結構必須具有與C結構相同的記憶體佈局時,您可能別無選擇,只能使用unsafe。關於指標操作,在unsafe包官方定義裡有四個描述:
- 任何型別的指標都可以被轉化為Pointer
- Pointer可以被轉化為任何型別的指標
- uintptr可以被轉化為Pointer
- Pointer可以被轉化為uintptr
額外在加上一個規則:指向不同型別資料的指標,是無法直接相互轉換的,必須藉助unsafe.Pointer(類似於C的 void指標)代理一下再轉換也就是利用上述的1,2規則。
舉例:
func Float64bits(f float64) uint64 { // 無法直接轉換,報錯:Connot convert expression of type *float64 to type *uint64 // return *(*uint64)(&f) // 先把*float64 轉成 Pointer(描述1),再把Pointer轉成*uint64(描述2) return *(*uint64)(unsafe.Pointer(&f)) }
二、unsafe的定義:
整體程式碼比較簡單,2個型別定義和3個uintptr的返回函式
package unsafe //ArbitraryType僅用於文件目的,實際上並不是unsafe包的一部分,它表示任意Go表示式的型別。 type ArbitraryType int //任意型別的指標,類似於C的*void type Pointer *ArbitraryType //確定結構在記憶體中佔用的確切大小 func Sizeof(x ArbitraryType) uintptr //返回結構體中某個field的偏移量 func Offsetof(x ArbitraryType) uintptr //返回結構體中某個field的對其值(位元組對齊的原因) func Alignof(x ArbitraryType) uintptr
看一個栗子:
package main import ( "fmt" "unsafe" ) type Human struct { sexbool ageuint8 minint name string } func main() { h := Human{ true, 30, 1, "hello", } i := unsafe.Sizeof(h) j := unsafe.Alignof(h.age ) k := unsafe.Offsetof(h.name) fmt.Println(i, j, k) fmt.Printf("%p\n", &h) var p unsafe.Pointer p = unsafe.Pointer(&h) fmt.Println(p) } //輸出 //32 1 16 //0xc00000a080 //0xc00000a080 // 32:string 佔16位元組,所以16+16 =32;1 是因為age前是bool,佔用1個位元組;8是name的偏移是int 佔8個字 節
三、Pointer使用
前面已經說了,pointer是任意型別的指標,可以指向任意型別資料。參照Float64bits的轉換和上述栗子的unsafe.Pointer(&h) ,所以主要用於轉換各種型別
四、uintptr
在golang中uintptr的定義是type uintptr uintptr uintptr是golang的內建型別,是能儲存指標的整型
- 根據描述3,一個unsafe.Pointer指標也可以被轉化為uintptr型別,然後儲存到指標型數值變數中(注:這只是和當前指標相同的一個數字值,並不是一個指標),然後用以做必要的指標數值運算。(uintptr是一個無符號的整型數,足以儲存一個地址)
- 這種轉換雖然也是可逆的,但是將uintptr轉為unsafe.Pointer指標可能會破壞型別系統,因為並不是所有的數字都是有效的記憶體地址。
- 許多將unsafe.Pointer指標轉為uintptr,然後再轉回為unsafe.Pointer型別指標的操作也是不安全的。比如下面的例子需要將變數x的地址加上b欄位地址偏移量轉化為*int16型別指標,然後通過該指標更新x.b:
package main import ( "fmt" "unsafe" ) func main() { var x struct { a bool b int16 c []int } /** unsafe.Offsetof 函式的引數必須是一個欄位 x.f, 然後返回 f 欄位相對於 x 起始地址的偏移量, 包括可能的空洞. */ /** uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b) 指標的運算 */ // 和 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地址。第三個向之前無效地址空間的賦值語句將徹底摧毀整個程式!