原文地址: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
是不是可以解決你的問題,這是我一點提醒。