看過 Go 相關源碼的同學,應該會注意到不少地方使用了 unsafe.Pointer 和 uintptr,單從類型名稱看,這些與“指針”是不是有什么關系?
先附上一張關系圖,后面我們再展開解析。

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