淺析 unsafe.Pointer 與 uintptr
看過 Go 相關(guān)源碼的同學(xué),應(yīng)該會(huì)注意到不少地方使用了 unsafe.Pointer 和 uintptr,單從類型名稱看,這些與“指針”是不是有什么關(guān)系?
先附上一張關(guān)系圖,后面我們?cè)僬归_解析。

普通指針類型
我們一般將 *T 看作指針類型,表示一個(gè)指向 T 類型變量的指針。我們都知道 Go 是強(qiáng)類型語(yǔ)言,聲明變量之后,變量的類型是不可以改變的,不同類型的指針也不允許相互轉(zhuǎn)化。例如下面這樣:
func?main(){
?i?:=?30
?iPtr1?:=?&i
?var?iPtr2?*int64?=?(*int64)(iPtr1)
?fmt.Println(iPtr2)
}
編譯報(bào)錯(cuò):cannot convert iPtr1 (type *int) to type *int64,提示不能進(jìn)行強(qiáng)制轉(zhuǎn)化。
那怎么辦,如何實(shí)現(xiàn)相互轉(zhuǎn)化?
還好 Go 官方提供了 unsafe 包,有相關(guān)的解決方案。
unsafe.Pointer
unsafe.Pointer 通用指針類型,一種特殊類型的指針,可以包含任意類型的地址,能實(shí)現(xiàn)不同的指針類型之間進(jìn)行轉(zhuǎn)換,類似于 C 語(yǔ)言里的 void* 指針。
type?ArbitraryType?int
type?Pointer?*ArbitraryType
從定義可以看出,Pointer 實(shí)際上是 *int。
官方文檔里還描述了 Pointer 的四種操作規(guī)則:
任何類型的指針都可以轉(zhuǎn)化成 unsafe.Pointer; unsafe.Pointer 可以轉(zhuǎn)化成任何類型的指針; uintptr 可以轉(zhuǎn)換為 unsafe.Pointer; unsafeP.ointer 可以轉(zhuǎn)換為 uintptr;
不同類型的指針允許相互轉(zhuǎn)化實(shí)際上是運(yùn)用了第 1、2 條規(guī)則,我們就著例子看下:
func?main(){
?i?:=?30
?iPtr1?:=?&i
?var?iPtr2?*int64?=?(*int64)(unsafe.Pointer(iPtr1))
?*iPtr2?=?8
?fmt.Println(i)
}
輸出:
8
上面的代碼,我們可以把 *int 轉(zhuǎn)為 *int64,并且對(duì)新的 *int64 進(jìn)行操作,從輸出會(huì)發(fā)現(xiàn) i 的值被改變了。
可以說 unsafe.Pointer 是橋梁,可以讓任意類型的指針實(shí)現(xiàn)相互轉(zhuǎn)換。
我們知道 Go 語(yǔ)言是不支持指針運(yùn)算,想要實(shí)現(xiàn)該怎么辦?
看看第 3、4 條規(guī)則,uintptr 就可以派上用場(chǎng)了。
uintptr
源碼定義:
//?uintptr?is?an?integer?type?that?is?large?enough?to?hold?the?bit?pattern?of
//?any?pointer.
type?uintptr?uintptr
uintptr 是 Go 內(nèi)置類型,表示無符號(hào)整數(shù),可存儲(chǔ)一個(gè)完整的地址。常用于指針運(yùn)算,只需將 unsafe.Pointer 類型轉(zhuǎn)換成 uintptr 類型,做完加減法后,轉(zhuǎn)換成 unsafe.Pointer,通過 * 操作,取值或者修改值都可以。
下面是一個(gè)通過指針偏移修改結(jié)構(gòu)體成員的例子,演示下 uintptr 的用法:
type?Admin?struct?{
?Name?string
?Age?int
}
func?main(){
?admin?:=?Admin{
??Name:?"seekload",
??Age:?18,
?}
?ptr?:=?&admin
?name?:=?(*string)(unsafe.Pointer(ptr))???//?1
?*name?=?"四哥"
?fmt.Println(*ptr)
?age?:=?(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr))?+?unsafe.Offsetof(ptr.Age)))??//?2
?*age?=?35
?fmt.Println(*ptr)
}
輸出:
{四哥?18}
{四哥?35}
特別提下,unsafe.Offsetof 的作用是返回成員變量 x 在結(jié)構(gòu)體當(dāng)中的偏移量,即返回結(jié)構(gòu)體初始內(nèi)存地址到 x 之間的字節(jié)數(shù)。
//1 因?yàn)?strong style="color: black;">結(jié)構(gòu)體初始地址就是第一個(gè)成員的地址,又 Name 是結(jié)構(gòu)體第一個(gè)成員變量,所以此處不用偏移,我們拿到 admin 的地址,然后通過 unsafe.Pointer 轉(zhuǎn)為 *string,再進(jìn)行賦值操作即可。
//2 成員變量 Age 不是第一個(gè)字段,想要修改它的值就需要內(nèi)存偏移。我們先將 admin 的指針轉(zhuǎn)化為 uintptr,再通過 unsafe.Offsetof() 獲取到 Age 的偏移量,兩者都是 uintptr,進(jìn)行相加指針運(yùn)算獲取到成員 Age 的地址,最后需要將 uintptr 轉(zhuǎn)化為 unsafe.Pointer,再轉(zhuǎn)化為 *int,才能對(duì) Age 操作。
總結(jié)
這篇文章我們簡(jiǎn)單介紹了普通指針類型、unsafe.Pointer 和 uintptr 之間的關(guān)系(見文章開頭關(guān)系圖),記住兩點(diǎn):
unsafe.Pointer 可以實(shí)現(xiàn)不同類型指針之間相互轉(zhuǎn)化; uintptr 搭配著 unsafe.Pointer 使用,實(shí)現(xiàn)指針運(yùn)算;
不過,官方不推薦使用 unsafe 包,正如它的命名一樣,是不安全的,比如涉及到內(nèi)存操作,這是繞過 Go 本身設(shè)計(jì)的安全機(jī)制的,不當(dāng)?shù)牟僮鳎赡軙?huì)破壞一塊內(nèi)存,而且這種問題非常不好定位。
推薦閱讀
