一春缕、unsafe 作用
從golang的定義來看,unsafe 是類型安全的操作艘蹋。顧名思義锄贼,它應(yīng)該非常謹(jǐn)慎地使用; unsafe可能很危險(xiǎn),但也可能非常有用女阀。例如宅荤,當(dāng)使用系統(tǒng)調(diào)用和Go結(jié)構(gòu)必須具有與C結(jié)構(gòu)相同的內(nèi)存布局時(shí),您可能別無選擇浸策,只能使用unsafe冯键。關(guān)于指針操作,在unsafe包官方定義里有四個(gè)描述:
- 任何類型的指針都可以被轉(zhuǎn)化為Pointer
- Pointer可以被轉(zhuǎn)化為任何類型的指針
- uintptr可以被轉(zhuǎn)化為Pointer
- Pointer可以被轉(zhuǎn)化為uintptr
額外在加上一個(gè)規(guī)則:指向不同類型數(shù)據(jù)的指針庸汗,是無法直接相互轉(zhuǎn)換的惫确,必須借助unsafe.Pointer(類似于C的 void指針)代理一下再轉(zhuǎn)換也就是利用上述的1,2規(guī)則。
舉例:
func Float64bits(f float64) uint64 {
// 無法直接轉(zhuǎn)換改化,報(bào)錯(cuò):Connot convert expression of type *float64 to type *uint64
// return *(*uint64)(&f)
// 先把*float64 轉(zhuǎn)成 Pointer(描述1)掩蛤,再把Pointer轉(zhuǎn)成*uint64(描述2)
return *(*uint64)(unsafe.Pointer(&f))
}
二、unsafe的定義:
整體代碼比較簡單陈肛,2個(gè)類型定義和3個(gè)uintptr的返回函數(shù)
package unsafe
//ArbitraryType僅用于文檔目的揍鸟,實(shí)際上并不是unsafe包的一部分,它表示任意Go表達(dá)式的類型。
type ArbitraryType int
//任意類型的指針燥爷,類似于C的*void
type Pointer *ArbitraryType
//確定結(jié)構(gòu)在內(nèi)存中占用的確切大小
func Sizeof(x ArbitraryType) uintptr
//返回結(jié)構(gòu)體中某個(gè)field的偏移量
func Offsetof(x ArbitraryType) uintptr
//返回結(jié)構(gòu)體中某個(gè)field的對(duì)其值(字節(jié)對(duì)齊的原因)
func Alignof(x ArbitraryType) uintptr
看一個(gè)栗子:
package main
import (
"fmt"
"unsafe"
)
type Human struct {
sex bool
age uint8
min int
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字節(jié)蜈亩,所以16+16 =32;1 是因?yàn)閍ge前是bool前翎,占用1個(gè)字節(jié)稚配;8是name的偏移是int 占8個(gè)字
節(jié)
三、Pointer使用
前面已經(jīng)說了港华,pointer是任意類型的指針道川,可以指向任意類型數(shù)據(jù)。參照Float64bits的轉(zhuǎn)換和上述栗子的unsafe.Pointer(&h)立宜,所以主要用于轉(zhuǎn)換各種類型
四冒萄、uintptr
在golang中uintptr的定義是 type uintptr uintptr uintptr是golang的內(nèi)置類型,是能存儲(chǔ)指針的整型
- 根據(jù)描述3橙数,一個(gè)unsafe.Pointer指針也可以被轉(zhuǎn)化為uintptr類型尊流,然后保存到指針型數(shù)值變量中(注:這只是和當(dāng)前指針相同的一個(gè)數(shù)字值,并不是一個(gè)指針)灯帮,然后用以做必要的指針數(shù)值運(yùn)算崖技。(uintptr是一個(gè)無符號(hào)的整型數(shù),足以保存一個(gè)地址)
- 這種轉(zhuǎn)換雖然也是可逆的钟哥,但是將uintptr轉(zhuǎn)為unsafe.Pointer指針可能會(huì)破壞類型系統(tǒng)迎献,因?yàn)椴⒉皇撬械臄?shù)字都是有效的內(nèi)存地址。
- 許多將unsafe.Pointer指針轉(zhuǎn)為uintptr腻贰,然后再轉(zhuǎn)回為unsafe.Pointer類型指針的操作也是不安全的吁恍。比如下面的例子需要將變量x的地址加上b字段地址偏移量轉(zhuǎn)化為*int16類型指針,然后通過該指針更新x.b:
package main
import (
"fmt"
"unsafe"
)
func main() {
var x struct {
a bool
b int16
c []int
}
/**
unsafe.Offsetof 函數(shù)的參數(shù)必須是一個(gè)字段 x.f, 然后返回 f 字段相對(duì)于 x 起始地址的偏移量, 包括可能的空洞.
*/
/**
uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
指針的運(yùn)算
*/
// 和 pb := &x.b 等價(jià)
pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
fmt.Println(x.b) // "42"
}
上面的寫法盡管很繁瑣播演,但在這里并不是一件壞事冀瓦,因?yàn)檫@些功能應(yīng)該很謹(jǐn)慎地使用。不要試圖引入一個(gè)uintptr類型的臨時(shí)變量写烤,因?yàn)樗赡軙?huì)破壞代碼的安全性(注:這是真正可以體會(huì)unsafe包為何不安全的例子)翼闽。
下面段代碼是錯(cuò)誤的:
// NOTE: subtly incorrect!
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42
產(chǎn)生錯(cuò)誤的原因很微妙。
有時(shí)候垃圾回收器會(huì)移動(dòng)一些變量以降低內(nèi)存碎片等問題顶霞。這類垃圾回收器被稱為移動(dòng)GC肄程。當(dāng)一個(gè)變量被移動(dòng)锣吼,所有的保存改變量舊地址的指針必須同時(shí)被更新為變量移動(dòng)后的新地址。從垃圾收集器的視角來看蓝厌,一個(gè)unsafe.Pointer是一個(gè)指向變量的指針玄叠,因此當(dāng)變量被移動(dòng)是對(duì)應(yīng)的指針也必須被更新;但是uintptr類型的臨時(shí)變量只是一個(gè)普通的數(shù)字拓提,所以其值不應(yīng)該被改變读恃。上面錯(cuò)誤的代碼因?yàn)橐胍粋€(gè)非指針的臨時(shí)變量tmp,導(dǎo)致垃圾收集器無法正確識(shí)別這個(gè)是一個(gè)指向變量x的指針代态。當(dāng)?shù)诙€(gè)語句執(zhí)行時(shí)寺惫,變量x可能已經(jīng)被轉(zhuǎn)移,這時(shí)候臨時(shí)變量tmp也就不再是現(xiàn)在的&x.b地址蹦疑。第三個(gè)向之前無效地址空間的賦值語句將徹底摧毀整個(gè)程序西雀!