轉(zhuǎn)載自: 你不知道的Go unsafe.Pointer uintptr原理和玩法
unsafe.Pointer
這個類型比較重要没讲,它是實(shí)現(xiàn)定位和讀寫的內(nèi)存的基礎(chǔ)灭忠,Go runtime大量使用它扣墩。官方文檔對該類型有四個重要描述:
(1)任何類型的指針都可以被轉(zhuǎn)化為Pointer
(2)Pointer可以被轉(zhuǎn)化為任何類型的指針
(3)uintptr可以被轉(zhuǎn)化為Pointer
(4)Pointer可以被轉(zhuǎn)化為uintptr
大多數(shù)指針類型會寫成T骤视,表示是“一個指向T類型變量的指針”。unsafe.Pointer是特別定義的一種指針類型(譯注:類似C語言中的void類型的指針)胁编,它可以包含任意類型變量的地址。當(dāng)然鳞尔,我們不可以直接通過*p來獲取unsafe.Pointer指針指向的真實(shí)變量的值嬉橙,因?yàn)槲覀儾⒉恢雷兞康木唧w類型。和普通指針一樣寥假,unsafe.Pointer指針也是可以比較的市框,并且支持和nil常量比較判斷是否為空指針。
一個普通的T類型指針可以被轉(zhuǎn)化為unsafe.Pointer類型指針糕韧,并且一個unsafe.Pointer類型指針也可以被轉(zhuǎn)回普通的指針枫振,被轉(zhuǎn)回普通的指針類型并不需要和原始的T類型相同。
package main
import (
"fmt"
"unsafe"
"reflect"
)
type W struct {
b byte
i int32
j int64
}
//通過將float64類型指針轉(zhuǎn)化為uint64類型指針萤彩,我們可以查看一個浮點(diǎn)數(shù)變量的位模式粪滤。
func Float64bits(f float64) uint64 {
fmt.Println(reflect.TypeOf(unsafe.Pointer(&f))) //unsafe.Pointer
fmt.Println(reflect.TypeOf((*uint64)(unsafe.Pointer(&f)))) //*uint64
return *(*uint64)(unsafe.Pointer(&f))
}
func Uint(i int)uint{
return *(*uint)(unsafe.Pointer(&i))
}
type Uint6 struct {
low [2]byte
high uint32
}
//func (u *Uint6) SetLow() {
// fmt.Printf("i=%d\n", this.i)
//}
//
//func (u *Uint6) SetHigh() {
// fmt.Printf("j=%d\n", this.j)
//}
func writeByPointer(){
uint6 := &Uint6{}
lowPointer:=(*[2]byte)(unsafe.Pointer(uint6))
*lowPointer = [2]byte{1,2}
//unsafe.Offsetof會計算padding后的偏移距離
//必須將unsafe.Pointer轉(zhuǎn)化成 uintptr類型才能進(jìn)行指針的運(yùn)算,uintptr 與 unsafe.Pointer 之間可以相互轉(zhuǎn)換雀扶。
highPointer:=(*uint32)(unsafe.Pointer(uintptr(unsafe.Pointer(uint6))+unsafe.Offsetof(uint6.high)))
fmt.Printf("addr %x addr %x size %v size %v size %v align %v offset %v \n", uintptr(unsafe.Pointer(uint6)),uintptr(unsafe.Pointer(uint6))+unsafe.Sizeof(uint6.low),unsafe.Sizeof([2]byte{1,2}),unsafe.Sizeof(uint6.low), unsafe.Sizeof(uint6.high), unsafe.Alignof(uint6.low), unsafe.Offsetof(uint6.high))
*highPointer = uint32(9)
//借助于 unsafe.Pointer杖小,我們實(shí)現(xiàn)了像 C 語言中的指針偏移操作。可以看出予权,這種不安全的操作使得我們可以在任何地方直接訪問結(jié)構(gòu)體中未公開的成員昂勉,只要能得到這個結(jié)構(gòu)體變量的地址。
fmt.Printf("%+v %v %v %v \n", uint6, &uint6,&uint6.low[0], &uint6.high)
}
type T struct {
t1 byte
t2 int32
t3 int64
t4 string
t5 bool
}
func main() {
fmt.Printf("%#x %#b \n", Float64bits(11.3), Float64bits(4)) // "0x3ff0000000000000"
var intA int =99
uintA:=Uint(intA)
fmt.Printf("%#v %v %v \n", intA, reflect.TypeOf(uintA), uintA)
var w W = W{}
//在struct中扫腺,它的對齊值是它的成員中的最大對齊值岗照。
fmt.Printf("%v, %v, %v, %v, %v, %v, %v, %v\n", unsafe.Alignof(w), unsafe.Alignof(w.b), unsafe.Alignof(w.i), unsafe.Alignof(w.j), unsafe.Sizeof(w),unsafe.Sizeof(w.b),unsafe.Sizeof(w.i),unsafe.Sizeof(w.j), )
fmt.Println(unsafe.Alignof(byte(0)))
fmt.Println(unsafe.Alignof(int8(0)))
fmt.Println(unsafe.Alignof(uint8(0)))
fmt.Println(unsafe.Alignof(int16(0)))
fmt.Println(unsafe.Alignof(uint16(0)))
fmt.Println(unsafe.Alignof(int32(0)))
fmt.Println(unsafe.Alignof(uint32(0)))
fmt.Println(unsafe.Alignof(int64(0)))
fmt.Println(unsafe.Alignof(uint64(0)))
fmt.Println(unsafe.Alignof(uintptr(0)))
fmt.Println(unsafe.Alignof(float32(0)))
fmt.Println(unsafe.Alignof(float64(0)))
//fmt.Println(unsafe.Alignof(complex(0, 0)))
fmt.Println(unsafe.Alignof(complex64(0)))
fmt.Println(unsafe.Alignof(complex128(0)))
fmt.Println(unsafe.Alignof(""))
fmt.Println(unsafe.Alignof(new(int)))
fmt.Println(unsafe.Alignof(struct {
f float32
ff float64
}{}))
fmt.Println(unsafe.Alignof(make(chan bool, 10)))
fmt.Println(unsafe.Alignof(make([]int, 10)))
fmt.Println(unsafe.Alignof(make(map[string]string, 10)))
t := &T{1, 2, 3, "", true}
fmt.Println("sizeof :")
fmt.Println(unsafe.Sizeof(*t))
fmt.Println(unsafe.Sizeof(t.t1))
fmt.Println(unsafe.Sizeof(t.t2))
fmt.Println(unsafe.Sizeof(t.t3))
fmt.Println(unsafe.Sizeof(t.t4))
fmt.Println(unsafe.Sizeof(t.t5))
//這里以0x0作為基準(zhǔn)內(nèi)存地址。打印出來總共占用40個字節(jié)笆环。t.t1 為 char谴返,對齊值為 1,0x0 % 1 == 0咧织,從0x0開始嗓袱,占用一個字節(jié);t.t2 為 int32习绢,對齊值為 4渠抹,0x4 % 4 == 0,從 0x4 開始闪萄,占用 4 個字節(jié)梧却;t.t3 為 int64,對齊值為 8败去,0x8 % 8 == 0放航,從 0x8 開始,占用 8 個字節(jié)圆裕;t.t4 為 string广鳍,對齊值為 8,0x16 % 8 == 0吓妆,從 0x16 開始赊时, 占用 16 個字節(jié)(string 內(nèi)部實(shí)現(xiàn)是一個結(jié)構(gòu)體,包含一個字節(jié)類型指針和一個整型的長度值)行拢;t.t5 為 bool祖秒,對齊值為 1,0x32 % 8 == 0舟奠,從 0x32 開始竭缝,占用 1 個字節(jié)。從上面分析沼瘫,可以知道 t 的對齊值為 8抬纸,最后 bool 之后會補(bǔ)齊到 8 的倍數(shù),故總共是 40 個字節(jié)晕鹊。
fmt.Println("Offsetof : ")
fmt.Println(unsafe.Offsetof(t.t1))
fmt.Println(unsafe.Offsetof(t.t2))
fmt.Println(unsafe.Offsetof(t.t3))
fmt.Println(unsafe.Offsetof(t.t4))
fmt.Println(unsafe.Offsetof(t.t5))
writeByPointer()
//CPU看待內(nèi)存是以block為單位的松却,就像是linux下文件大小的單位IO block為4096一樣暴浦,
//是一種犧牲空間換取時間的做法, 我們一定要注意不要浪費(fèi)空間,
//struct類型定義的時候一定要將占用內(nèi)從空間小的類型放在前面, 充足利用padding晓锻, 才能提升內(nèi)存歌焦、cpu效率
}
go run PLAY.go
unsafe.Pointer
*uint64
unsafe.Pointer
*uint64
0x402699999999999a 0b100000000010000000000000000000000000000000000000000000000000000
99 uint 99
8, 1, 4, 8, 16, 1, 4, 8
1
1
1
2
2
4
4
8
8
8
4
8
4
8
8
8
8
8
8
8
sizeof :
40
1
4
8
16
1
Offsetof :
0
4
8
16
32
addr c00008e038 addr c00008e03a size 2 size 2 size 4 align 1 offset 4
&{low:[1 2] high:9} 0xc00008a010 0xc00008e038 0xc00008e03c
uintptr
// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr
uintptr是golang的內(nèi)置類型,是能存儲指針的整型砚哆,在64位平臺上底層的數(shù)據(jù)類型是独撇,
typedef unsigned long long int uint64;
typedef uint64 uintptr;
一個unsafe.Pointer指針也可以被轉(zhuǎn)化為uintptr類型,然后保存到指針型數(shù)值變量中(注:這只是和當(dāng)前指針相同的一個數(shù)字值躁锁,并不是一個指針)纷铣,然后用以做必要的指針數(shù)值運(yùn)算。(uintptr是一個無符號的整型數(shù)战转,足以保存一個地址)這種轉(zhuǎn)換雖然也是可逆的搜立,但是將uintptr轉(zhuǎn)為unsafe.Pointer指針可能會破壞類型系統(tǒng),因?yàn)椴⒉皇撬械臄?shù)字都是有效的內(nèi)存地址槐秧。
許多將unsafe.Pointer指針轉(zhuǎn)為原生數(shù)字啄踊,然后再轉(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ù)必須是一個字段 x.f, 然后返回 f 字段相對于 x 起始地址的偏移量, 包括可能的空洞.
*/
/**
uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
指針的運(yùn)算
*/
// 和 pb := &x.b 等價
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)慎地使用膀懈。不要試圖引入一個uintptr類型的臨時變量顿锰,因?yàn)樗赡軙茐拇a的安全性(注:這是真正可以體會unsafe包為何不安全的例子)。
下面段代碼是錯誤的:
// NOTE: subtly incorrect!
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42
產(chǎn)生錯誤的原因很微妙启搂。有時候垃圾回收器會移動一些變量以降低內(nèi)存碎片等問題硼控。這類垃圾回收器被稱為移動GC。當(dāng)一個變量被移動狐血,所有的保存改變量舊地址的指針必須同時被更新為變量移動后的新地址淀歇。從垃圾收集器的視角來看,一個unsafe.Pointer是一個指向變量的指針匈织,因此當(dāng)變量被移動是對應(yīng)的指針也必須被更新;但是uintptr類型的臨時變量只是一個普通的數(shù)字牡直,所以其值不應(yīng)該被改變缀匕。上面錯誤的代碼因?yàn)橐胍粋€非指針的臨時變量tmp,導(dǎo)致垃圾收集器無法正確識別這個是一個指向變量x的指針碰逸。當(dāng)?shù)诙€語句執(zhí)行時乡小,變量x可能已經(jīng)被轉(zhuǎn)移,這時候臨時變量tmp也就不再是現(xiàn)在的&x.b地址饵史。第三個向之前無效地址空間的賦值語句將徹底摧毀整個程序满钟!
總結(jié)
第一是 unsafe.Pointer
可以讓你的變量在不同的指針類型轉(zhuǎn)來轉(zhuǎn)去胜榔,也就是表示為任意可尋址的指針類型。第二是 uintptr
常用于與 unsafe.Pointer
打配合湃番,用于做指針運(yùn)算夭织,和C (*void)指針一樣。
unsafe是不安全的吠撮,所以我們應(yīng)該盡可能少的使用它尊惰,比如內(nèi)存的操縱,這是繞過Go本身設(shè)計的安全機(jī)制的泥兰,不當(dāng)?shù)牟僮髋牛赡軙茐囊粔K內(nèi)存,而且這種問題非常不好定位鞋诗。
當(dāng)然必須的時候我們可以使用它膀捷,比如底層類型相同的數(shù)組之間的轉(zhuǎn)換;比如使用sync/atomic包中的一些函數(shù)時削彬;還有訪問Struct的私有字段時全庸;該用還是要用,不過一定要慎之又慎吃警。
還有糕篇,整個unsafe包都是用于Go編譯器的,不用運(yùn)行時酌心,在我們編譯的時候拌消,Go編譯器已經(jīng)把他們都處理了。