上一篇文章咱們詳細分析了 map 的底層實現,若是你也跟着閱讀了源碼,那必定對 unsafe.Pointer
不陌生,map 對 key 進行定位的時候,大量使用。html
unsafe.Pointer
位於 unsafe 包
,這篇文章,咱們來深刻研究 unsafe 包。先說明一下,本文沒有以前那麼長了,你能夠比較輕鬆地讀完,這樣的時候不是太多。git
上次發佈文章的時候,包括代碼超過 5w 字,後臺編輯器的體驗很是差,一度讓我懷疑人生。我以前說過,像 map 那樣的長文,估計能讀完的不超過 1 %
。像下面這幾位同窗的評價,並很少見。程序員
我的認爲,學習自己並非一件輕鬆愉快的事情,寓教於樂是個美好的願望。想要深入地領悟,就得付出別人看不見的努力。學習歷來都不會是一件輕鬆的事情,枯燥是正常的。耐住性子,深刻研究某個問題,讀書、看文章、寫博客均可以,浮躁時代作個專一的人!github
在正式介紹 unsafe 包以前,須要着重介紹 Go 語言中的指針類型。golang
我本科開始學編程的時候,第一門語言就是 C。以後又陸續學過 C++,Java,Python,這些語言都挺強大的,可是沒了 C 語言那麼「單純」。直到我開始接觸 Go 語言,又找到了那種感受。Go 語言的做者之一 Ken Thompson 也是 C 語言的做者。因此,Go 能夠看做 C 系語言,它的不少特性都和 C 相似,指針就是其中之一。shell
然而,Go 語言的指針相比 C 的指針有不少限制。這固然是爲了安全考慮,要知道像 Java/Python 這些現代語言,生怕程序員出錯,哪有什麼指針(這裏指的是顯式的指針)?更別說像 C/C++ 還須要程序員本身清理「垃圾」。因此對於 Go 來講,有指針已經很不錯了,僅管它有不少限制。編程
爲何須要指針類型呢?參考文獻 go101.org 裏舉了這樣一個例子:segmentfault
package main import "fmt" func double(x int) { x += x } func main() { var a = 3 double(a) fmt.Println(a) // 3 }
很是簡單,我想在 double 函數裏將 a 翻倍,可是例子中的函數卻作不到。爲何?由於 Go 語言的函數傳參都是值傳遞
。double 函數裏的 x 只是實參 a 的一個拷貝,在函數內部對 x 的操做不能反饋到實參 a。數組
若是這時,有一個指針就能夠解決問題了!這也是咱們經常使用的「伎倆」。安全
package main import "fmt" func double(x *int) { *x += *x x = nil } func main() { var a = 3 double(&a) fmt.Println(a) // 6 p := &a double(p) fmt.Println(a, p == nil) // 12 false }
很常規的操做,不用多解釋。惟一可能有些疑惑的在這一句:
x = nil
這得稍微思考一下,才能得出這一行代碼根本不影響的結論。由於是值傳遞,因此 x 也只是對 &a 的一個拷貝。
*x += *x
這一句把 x 指向的值(也就是 &a 指向的值,即變量 a)變爲原來的 2 倍。可是對 x 自己(一個指針)的操做卻不會影響外層的 a,因此 x = nil
掀不起任何大風大浪。
下面的這張圖能夠「自證清白」:
然而,相比於 C 語言中指針的靈活,Go 的指針多了一些限制。但這也算是 Go 的成功之處:既能夠享受指針帶來的便利,又避免了指針的危險性。
限制一:Go 的指針不能進行數學運算
。
來看一個簡單的例子:
a := 5 p := &a p++ p = &a + 3
上面的代碼將不能經過編譯,會報編譯錯誤:invalid operation
,也就是說不能對指針作數學運算。
限制二:不一樣類型的指針不能相互轉換
。
例以下面這個簡短的例子:
func main() { a := int(100) var f *float64 f = &a }
也會報編譯錯誤:
cannot use &a (type *int) as type *float64 in assignment
關於兩個指針可否相互轉換,參考資料中 go 101 相關文章裏寫得很是細,這裏我不想展開。我的認爲記住這些沒有什麼意義,有完美主義的同窗能夠去閱讀原文。固然我也有完美主義,但我有時會剋制,嘿嘿。
限制三:不一樣類型的指針不能使用 == 或 != 比較
。
只有在兩個指針類型相同或者能夠相互轉換的狀況下,才能夠對二者進行比較。另外,指針能夠經過 ==
和 !=
直接和 nil
做比較。
限制四:不一樣類型的指針變量不能相互賦值
。
這一點同限制三。
前面所說的指針是類型安全的,但它有不少限制。Go 還有非類型安全的指針,這就是 unsafe 包提供的 unsafe.Pointer。在某些狀況下,它會使代碼更高效,固然,也更危險。
unsafe 包用於 Go 編譯器,在編譯階段使用。從名字就能夠看出來,它是不安全的,官方並不建議使用。我在用 unsafe 包的時候會有一種不舒服的感受,可能這也是語言設計者的意圖吧。
可是高階的 Gopher,怎麼能不會使用 unsafe 包呢?它能夠繞過 Go 語言的類型系統,直接操做內存。例如,通常咱們不能操做一個結構體的未導出成員,可是經過 unsafe 包就能作到。unsafe 包讓我能夠直接讀寫內存,還管你什麼導出仍是未導出。
Go 語言類型系統是爲了安全和效率設計的,有時,安全會致使效率低下。有了 unsafe 包,高階的程序員就能夠利用它繞過類型系統的低效。所以,它就有了存在的意義,閱讀 Go 源碼,會發現有大量使用 unsafe 包的例子。
咱們來看源碼:
type ArbitraryType int type Pointer *ArbitraryType
從命名來看,Arbitrary
是任意的意思,也就是說 Pointer 能夠指向任意類型,實際上它相似於 C 語言裏的 void*
。
unsafe 包還有其餘三個函數:
func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr
Sizeof
返回類型 x 所佔據的字節數,但不包含 x 所指向的內容的大小。例如,對於一個指針,函數返回的大小爲 8 字節(64位機上),一個 slice 的大小則爲 slice header 的大小。
Offsetof
返回結構體成員在內存中的位置離結構體起始處的字節數,所傳參數必須是結構體的成員。
Alignof
返回 m,m 是指當類型進行內存對齊時,它分配到的內存地址能整除 m。
注意到以上三個函數返回的結果都是 uintptr 類型,這和 unsafe.Pointer 能夠相互轉換。三個函數都是在編譯期間執行,它們的結果能夠直接賦給 const 型變量
。另外,由於三個函數執行的結果和操做系統、編譯器相關,因此是不可移植的。
綜上所述,unsafe 包提供了 2 點重要的能力:
- 任何類型的指針和 unsafe.Pointer 能夠相互轉換。
- uintptr 類型和 unsafe.Pointer 能夠相互轉換。
pointer 不能直接進行數學運算,但能夠把它轉換成 uintptr,對 uintptr 類型進行數學運算,再轉換成 pointer 類型。
// uintptr 是一個整數類型,它足夠大,能夠存儲 type uintptr uintptr
還有一點要注意的是,uintptr 並無指針的語義,意思就是 uintptr 所指向的對象會被 gc 無情地回收。而 unsafe.Pointer 有指針語義,能夠保護它所指向的對象在「有用」的時候不會被垃圾回收。
unsafe 包中的幾個函數都是在編譯期間執行完畢,畢竟,編譯器對內存分配這些操做「瞭然於胸」。在 /usr/local/go/src/cmd/compile/internal/gc/unsafe.go
路徑下,能夠看到編譯期間 Go 對 unsafe 包中函數的處理。
更深層的原理須要去研究編譯器的源碼,這裏就不去深究了。咱們重點關注它的用法,接着往下看。
經過前面關於 slice 的文章,咱們知道了 slice header 的結構體定義:
// runtime/slice.go type slice struct { array unsafe.Pointer // 元素指針 len int // 長度 cap int // 容量 }
調用 make 函數新建一個 slice,底層調用的是 makeslice 函數,返回的是 slice 結構體:
func makeslice(et *_type, len, cap int) slice
所以咱們能夠經過 unsafe.Pointer 和 uintptr 進行轉換,獲得 slice 的字段值。
func main() { s := make([]int, 9, 20) var Len = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8))) fmt.Println(Len, len(s)) // 9 9 var Cap = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16))) fmt.Println(Cap, cap(s)) // 20 20 }
Len,cap 的轉換流程以下:
Len: &s => pointer => uintptr => pointer => *int => int Cap: &s => pointer => uintptr => pointer => *int => int
再來看一下上篇文章咱們講到的 map:
type hmap struct { count int flags uint8 B uint8 noverflow uint16 hash0 uint32 buckets unsafe.Pointer oldbuckets unsafe.Pointer nevacuate uintptr extra *mapextra }
和 slice 不一樣的是,makemap 函數返回的是 hmap 的指針,注意是指針:
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap
咱們依然能經過 unsafe.Pointer 和 uintptr 進行轉換,獲得 hamp 字段的值,只不過,如今 count 變成二級指針了:
func main() { mp := make(map[string]int) mp["qcrao"] = 100 mp["stefno"] = 18 count := **(**int)(unsafe.Pointer(&mp)) fmt.Println(count, len(mp)) // 2 2 }
count 的轉換過程:
&mp => pointer => **int => int
在 map 源碼中,mapaccess一、mapassign、mapdelete 函數中,須要定位 key 的位置,會先對 key 作哈希運算。
例如:
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + (hash&m)*uintptr(t.bucketsize)))
h.buckets
是一個 unsafe.Pointer
,將它轉換成 uintptr
,而後加上 (hash&m)*uintptr(t.bucketsize)
,兩者相加的結果再次轉換成 unsafe.Pointer
,最後,轉換成 bmap 指針
,獲得 key 所落入的 bucket 位置。若是不熟悉這個公式,能夠看看上一篇文章,淺顯易懂。
上面舉的例子相對簡單,來看一個關於賦值的更難一點的例子:
// store new key/value at insert position if t.indirectkey { kmem := newobject(t.key) *(*unsafe.Pointer)(insertk) = kmem insertk = kmem } if t.indirectvalue { vmem := newobject(t.elem) *(*unsafe.Pointer)(val) = vmem } typedmemmove(t.key, insertk, key)
這段代碼是在找到了 key 要插入的位置後,進行「賦值」操做。insertk 和 val 分別表示 key 和 value 所要「放置」的地址。若是 t.indirectkey 爲真,說明 bucket 中存儲的是 key 的指針,所以須要將 insertk 當作指針的指針
,這樣才能將 bucket 中的相應位置的值設置成指向真實 key 的地址值,也就是說 key 存放的是指針。
下面這張圖展現了設置 key 的所有操做:
obj 是真實的 key 存放的地方。第 4 號圖,obj 表示執行完 typedmemmove
函數後,被成功賦值。
對於一個結構體,經過 offset 函數能夠獲取結構體成員的偏移量,進而獲取成員的地址,讀寫該地址的內存,就能夠達到改變成員值的目的。
這裏有一個內存分配相關的事實:結構體會被分配一塊連續的內存,結構體的地址也表明了第一個成員的地址。
咱們來看一個例子:
package main import ( "fmt" "unsafe" ) type Programmer struct { name string language string } func main() { p := Programmer{"stefno", "go"} fmt.Println(p) name := (*string)(unsafe.Pointer(&p)) *name = "qcrao" lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.language))) *lang = "Golang" fmt.Println(p) }
運行代碼,輸出:
{stefno go} {qcrao Golang}
name 是結構體的第一個成員,所以能夠直接將 &p 解析成 *string。這一點,在前面獲取 map 的 count 成員時,用的是一樣的原理。
對於結構體的私有成員,如今有辦法能夠經過 unsafe.Pointer 改變它的值了。
我把 Programmer 結構體升級,多加一個字段:
type Programmer struct { name string age int language string }
而且放在其餘包,這樣在 main 函數中,它的三個字段都是私有成員變量,不能直接修改。但我經過 unsafe.Sizeof() 函數能夠獲取成員大小,進而計算出成員的地址,直接修改內存。
func main() { p := Programmer{"stefno", 18, "go"} fmt.Println(p) lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(int(0)) + unsafe.Sizeof(string("")))) *lang = "Golang" fmt.Println(p) }
輸出:
{stefno 18 go} {stefno 18 Golang}
這是一個很是精典的例子。實現字符串和 bytes 切片之間的轉換,要求是 zero-copy
。想一下,通常的作法,都須要遍歷字符串或 bytes 切片,再挨個賦值。
完成這個任務,咱們須要瞭解 slice 和 string 的底層數據結構:
type StringHeader struct { Data uintptr Len int } type SliceHeader struct { Data uintptr Len int Cap int }
上面是反射包下的結構體,路徑:src/reflect/value.go。只須要共享底層 []byte 數組就能夠實現 zero-copy
。
func string2bytes(s string) []byte { stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s)) bh := reflect.SliceHeader{ Data: stringHeader.Data, Len: stringHeader.Len, Cap: stringHeader.Len, } return *(*[]byte)(unsafe.Pointer(&bh)) } func bytes2string(b []byte) string{ sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b)) sh := reflect.StringHeader{ Data: sliceHeader.Data, Len: sliceHeader.Len, } return *(*string)(unsafe.Pointer(&sh)) }
代碼比較簡單,不做詳細解釋。經過構造 slice header 和 string header,來完成 string 和 byte slice 之間的轉換。
unsafe 包繞過了 Go 的類型系統,達到直接操做內存的目的,使用它有必定的風險性。可是在某些場景下,使用 unsafe 包提供的函數會提高代碼的效率,Go 源碼中也是大量使用 unsafe 包。
unsafe 包定義了 Pointer 和三個函數:
type ArbitraryType int type Pointer *ArbitraryType func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr
經過三個函數能夠獲取變量的大小、偏移、對齊等信息。
uintptr 能夠和 unsafe.Pointer 進行相互轉換,uintptr 能夠進行數學運算。這樣,經過 uintptr 和 unsafe.Pointer 的結合就解決了 Go 指針不能進行數學運算的限制。
經過 unsafe 相關函數,能夠獲取結構體私有成員的地址,進而對其作進一步的讀寫操做,突破 Go 的類型安全限制。關於 unsafe 包,咱們更多關注它的用法。
順便說一句,unsafe 包用多了以後,也不以爲它的名字有多麼地不「美觀」了。相反,由於使用了官方並不提倡的東西,反而以爲有點酷炫。這就是叛逆的感受吧。
最後,點擊閱讀原文,你將參與見證一個千星項目的成長,你值得擁有!
【飛雪無情的博客】https://www.flysnow.org/2017/...
【譯文 unsafe包詳解】https://gocn.vip/question/371
【官方文檔】https://golang.org/pkg/unsafe/
【例子】http://www.opscoder.info/gola...
【煎魚大佬的博客】https://segmentfault.com/a/11...
【go語言聖經】https://www.kancloud.cn/wizar...
【pointer and system calls】https://blog.gopheracademy.co...
【pointer and uintptr】https://my.oschina.net/xinxin...
【unsafe.pointer】https://go101.org/article/uns...
【go 指針類型】https://go101.org/article/poi...
【碼洞 快學Go語言 unsafe】https://juejin.im/post/5c189d...
【官方文檔】https://golang.org/pkg/unsafe/
【jasper 的小窩】http://www.opscoder.info/gola...