【Golang】Go unsafe Pointer

Go語言在設計的時候,爲了編寫方便、效率高以及降低複雜度,被設計成爲一門強類型的靜態語言。強類型意味着一旦定義了,它的類型就不能改變了;靜態意味着類型檢查在運行前就做了。

同時爲了安全的考慮,Go語言是不允許兩個指針類型進行轉換的。

指針類型轉換

我們一般使用T作爲一個指針類型,表示一個指向類型T變量的指針。爲了安全的考慮,兩個不同的指針類型不能相互轉換,比如int不能轉爲*float64。

func main() {
	i:= 10
	ip:=&i

	var fp *float64 = (*float64)(ip)
	
	fmt.Println(fp)
}

以上代碼我們在編譯的時候,會提示cannot convert ip (type *int) to type *float64,也就是不能進行強制轉型。那如果我們還是需要進行轉換怎麼做呢?這就需要我們使用unsafe包裏的Pointer了,下面我們先看看unsafe.Pointer是什麼,然後再介紹如何轉換。

Pointer

unsafe.Pointer是一種特殊意義的指針,它可以包含任意類型的地址,有點類似於C語言裏的void*指針,全能型的。

func main() {
	i:= 10
	ip:=&i

	var fp *float64 = (*float64)(unsafe.Pointer(ip))
	
	*fp = *fp * 3

	fmt.Println(i)
}

以上示例,我們可以把int轉爲float64,並且我們嘗試了對新的*float64進行操作,打印輸出i,就會發現i的址同樣被改變。

以上這個例子沒有任何實際的意義,但是我們說明了,通過unsafe.Pointer這個萬能的指針,我們可以在*T之間做任何轉換。

type ArbitraryType int

type Pointer *ArbitraryType

可以看到unsafe.Pointer其實就是一個*int,一個通用型的指針。

我們看下關於unsafe.Pointer的4個規則:

  1. 任何指針都可以轉換爲unsafe.Pointer
  2. unsafe.Pointer可以轉換爲任何指針
  3. uintptr可以轉換爲unsafe.Pointer
  4. unsafe.Pointer可以轉換爲uintptr

前面兩個規則我們剛剛已經演示了,主要用於T1和T2之間的轉換,那麼最後兩個規則是做什麼的呢?我們都知道*T是不能計算偏移量的,也不能進行計算,但是uintptr可以,所以我們可以把指針轉爲uintptr再進行偏移計算,這樣我們就可以訪問特定的內存了,達到對不同的內存讀寫的目的。

下面我們以通過指針偏移修改Struct結構體內的字段爲例,來演示uintptr的用法。

func main() {
	u:=new(user)
	fmt.Println(*u)

	pName:=(*string)(unsafe.Pointer(u))
	*pName="張三"

	pAge:=(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(u))+unsafe.Offsetof(u.age)))
	*pAge = 20

	fmt.Println(*u)
}

type user struct {
	name string
	age int
}

以上我們通過內存偏移的方式,定位到我們需要操作的字段,然後改變他們的值。

第一個修改user的name值的時候,因爲name是第一個字段,所以不用偏移,我們獲取user的指針,然後通過unsafe.Pointer轉爲*string進行賦值操作即可。

第二個修改user的age值的時候,因爲age不是第一個字段,所以我們需要內存偏移,內存偏移牽涉到的計算只能通過uintptr,所我們要先把user的指針地址轉爲uintptr,然後我們再通過unsafe.Offsetof(u.age)獲取需要偏移的值,進行地址運算(+)偏移即可。

現在偏移後,地址已經是user的age字段了,如果要給它賦值,我們需要把uintptr轉爲int纔可以。所以我們通過把uintptr轉爲unsafe.Pointer,再轉爲int就可以操作了。

這裏我們可以看到,我們第二個偏移的表達式非常長,但是也千萬不要把他們分段,不能像下面這樣。

temp:=uintptr(unsafe.Pointer(u))+unsafe.Offsetof(u.age)
pAge:=(*int)(unsafe.Pointer(temp))
*pAge = 20

邏輯上看,以上代碼不會有什麼問題,但是這裏會牽涉到GC,如果我們的這些臨時變量被GC,那麼導致的內存操作就錯了,我們最終操作的,就不知道是哪塊內存了,會引起莫名其妙的問題。

小結

unsafe是不安全的,所以我們應該儘可能少的使用它,比如內存的操縱,這是繞過Go本身設計的安全機制的,不當的操作,可能會破壞一塊內存,而且這種問題非常不好定位。

當然必須的時候我們可以使用它,比如底層類型相同的數組之間的轉換;比如使用sync/atomic包中的一些函數時;還有訪問Struct的私有字段時;該用還是要用,不過一定要慎之又慎。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章