Go unsafe.Pointer uintptr原理和玩法

轉(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
420532-20191108152333898-1937227467.png

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)把他們都處理了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末安券,一起剝皮案震驚了整個濱河市墩崩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌侯勉,老刑警劉巖鹦筹,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異址貌,居然都是意外死亡铐拐,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門练对,熙熙樓的掌柜王于貴愁眉苦臉地迎上來遍蟋,“玉大人,你說我怎么就攤上這事螟凭⌒榍啵” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵螺男,是天一觀的道長棒厘。 經(jīng)常有香客問我纵穿,道長,這世上最難降的妖魔是什么奢人? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任谓媒,我火速辦了婚禮,結(jié)果婚禮上达传,老公的妹妹穿的比我還像新娘篙耗。我一直安慰自己,他們只是感情好宪赶,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布宗弯。 她就那樣靜靜地躺著,像睡著了一般搂妻。 火紅的嫁衣襯著肌膚如雪蒙保。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天欲主,我揣著相機(jī)與錄音邓厕,去河邊找鬼。 笑死扁瓢,一個胖子當(dāng)著我的面吹牛详恼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播引几,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼昧互,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了伟桅?” 一聲冷哼從身側(cè)響起敞掘,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎楣铁,沒想到半個月后玖雁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡盖腕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年赫冬,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片溃列。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡面殖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出哭廉,到底是詐尸還是另有隱情,我是刑警寧澤相叁,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布遵绰,位于F島的核電站辽幌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏椿访。R本人自食惡果不足惜乌企,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望成玫。 院中可真熱鬧加酵,春花似錦、人聲如沸哭当。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽钦勘。三九已至陋葡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間彻采,已是汗流浹背腐缤。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肛响,地道東北人岭粤。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像特笋,于是被迫代替她去往敵國和親剃浇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容