golang unsafe.Pointer與uintptr

原文地址:https://blog.fanscore.cn/p/33/

先說結論

  • uintptr 是一個地址數值,它不是指針,與地址上的對象沒有引用關係,垃圾回收器不會因爲有一個uintptr類型的值指向某對象而不回收該對象。
  • unsafe.Pointer是一個指針,類似於C的void *,它與地址上的對象存在引用關係,垃圾回收器因爲有一個unsafe.Pointer類型的值指向某對象而不回收該對象。
  • 任何指針都可以轉爲unsafe.Pointer
  • unsafe.Pointer可以轉爲任何指針
  • uintptr可以轉換爲unsafe.Pointer
  • unsafe.Pointer可以轉換爲uintptr
  • 指針不能直接轉換爲uintptr

爲什麼需要uintptr這個類型呢?

理論上說指針不過是一個數值,即一個uint,但實際上在go中unsafe.Pointer是不能通過強制類型轉換爲一個uint的,只能將unsafe.Pointer強制類型轉換爲一個uintptr。

var v1 float64 = 1.1
var v2 *float64 = &v1
_ = int(v2) // 這裏編譯報錯:cannot convert unsafe.Pointer(v2) (type unsafe.Pointer) to type uint

但是可以將一個unsafe.Pointer強制類型轉換爲一個uintptr:

var v1 float64 = 1.1
var v2 *float64 = &v1
var v3 uintptr = uintptr(unsafe.Pointer(v2))
v4 := uint(v3)
fmt.Println(v3, v4) // v3和v4打印出來的值是相同的

可以理解爲uintptr是專門用來指針操作的uint。
另外需要指出的是指針不能直接轉爲uintptr,即

var a float64
uintptr(&a) 這裏會報錯,不允許將*float64轉爲uintptr

一個🌰

通過上面的描述如果你還是一頭霧水的話,不妨看下下面這個實際案例:

package foo

type Person struct {
	Name string
	age  int
}

上面的代碼中我們在foo包中定義了一個結構體Person,只導出了Name字段,而沒有導出age字段,就是說在另外的包中我們只能直接操作Person.Name而不能直接操作Person.age,但是利用unsafe包可以繞過這個限制使我們能夠操作Person.age

package main

func main() {
	p := &foo.Person{
		Name: "張三",
	}

	fmt.Println(p)
	// *Person是不能直接轉換爲*string的,所以這裏先將*Person轉爲unsafe.Pointer,再將unsafe.Pointer轉爲*string
	pName := (*string)(unsafe.Pointer(p)) 
	*pName = "李四"

	// 正常手段是不能操作Person.age的這裏先通過uintptr(unsafe.Pointer(pName))得到Person.Name的地址
	// 通過unsafe.Sizeof(p.Name)得到Person.Name佔用的字節數
	// Person.Name的地址 + Person.Name佔用的字節數就得到了Person.age的地址,然後將地址轉爲int指針。
	pAge := (*int)(unsafe.Pointer((uintptr(unsafe.Pointer(pName)) + unsafe.Sizeof(p.Name))))
	// 將p的age字段修改爲12
	*pAge = 12

	fmt.Println(p)
}

打印結果爲:

$ go run main.go
&{張三 0}
&{李四 12}

需要注意的是下面這段代碼比較長:

pAge := (*int)(unsafe.Pointer((uintptr(unsafe.Pointer(pName)) + unsafe.Sizeof(p.Name))))

但是儘量不要分成兩段代碼,像這樣:

temp := uintptr(unsafe.Pointer(pName)) + unsafe.Sizeof(p.Name))
pAge := (*int)(unsafe.Pointer(temp)

原因是在第二行語句時,已經沒有指針指向p了,這時p可能會回收掉了,這時得到的地址temp就是個野指針了,不知道指向誰了,是比較危險的。

另外一個原因是在當前Go(golang版本:1.14)的內存管理機制中不會遷移內存,但是不保證以後的版本內存管理機制中有遷移內存的操作,一旦發生了內存遷移指針地址發生變更,上面的分段代碼就有可能出現嚴重問題。

關於Go的內存管理可以參看這篇文章:https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/,讀完這篇文章相信你就能理解上面的內存遷移問題。

除了上面兩點外還有一個原因是在Go 1.3上,當棧需要增長時棧可能會發生移動,對於下面的代碼:

var obj int
fmt.Println(uintptr(unsafe.Pointer(&obj)))
bigFunc() // bigFunc()增大了棧
fmt.Println(uintptr(unsafe.Pointer(&obj)))

完全有可能打印出來兩個地址。

通過上面的例子應該明白了爲什麼這個包名爲unsafe,因爲使用起來確實有風險,所以儘量不要使用這個包。

我之所以研究unsafe.Pointer完全是因爲我要在多線程的環境中採用原子操作避免競爭問題,所以我用到了atomic.LoadPointer(addr *unsafe.Pointer)。不過我後面發現了atomic包提供了一個atomic.Value結構體,這個結構體提供的方法使我避免顯式使用了unsafe.Pointer。所以你也正在使用atomic.LoadPointer()不妨看看atomic.Value是不是可以解決你的問題,這是我一點提醒。

參考資料

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