淺析 unsafe.Pointer 與 uintptr(重要)


看過 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 的四種操作規則:

  1. 任何類型的指針都可以轉化成 unsafe.Pointer;
  2. unsafe.Pointer 可以轉化成任何類型的指針;
  3. uintptr 可以轉換為 unsafe.Pointer;
  4. 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 之間的關系(見文章開頭關系圖),記住兩點:

  1. unsafe.Pointer 可以實現不同類型指針之間相互轉化;
  2. uintptr 搭配着 unsafe.Pointer 使用,實現指針運算;

不過,官方不推薦使用 unsafe 包,正如它的命名一樣,是不安全的,比如涉及到內存操作,這是繞過 Go 本身設計的安全機制的,不當的操作,可能會破壞一塊內存,而且這種問題非常不好定位。

淺析 unsafe.Pointer 與 uintptr


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM