在上一篇文章 《深刻理解 Go Slice》 中,你們會發現其底層數據結構使用了 unsafe.Pointer
。所以想着再介紹一下其關聯知識git
原文地址:有點不安全卻又一亮的 Go unsafe.Pointergithub
在你們學習 Go 的時候,確定都學過 「Go 的指針是不支持指針運算和轉換」 這個知識點。爲何呢?golang
首先,Go 是一門靜態語言,全部的變量都必須爲標量類型。不一樣的類型不可以進行賦值、計算等跨類型的操做。那麼指針也對應着相對的類型,也在 Compile 的靜態類型檢查的範圍內。同時靜態語言,也稱爲強類型。也就是一旦定義了,就不能再改變它segmentfault
func main(){ num := 5 numPointer := &num flnum := (*float32)(numPointer) fmt.Println(flnum) }
輸出結果:安全
# command-line-arguments ...: cannot convert numPointer (type *int) to type *float32
在示例中,咱們建立了一個 num
變量,值爲 5,類型爲 int
。取了其對於的指針地址後,試圖強制轉換爲 *float32
,結果失敗...數據結構
針對剛剛的 「錯誤示例」,咱們能夠採用今天的男主角 unsafe
標準庫來解決。它是一個神奇的包,在官方的詮釋中,有以下概述:學習
簡單來說就是,不怎麼推薦你使用。由於它是 unsafe(不安全的),可是在特殊的場景下,使用了它。能夠打破 Go 的類型和內存安全機制,讓你得到眼前一亮的驚喜效果 😄ui
爲了解決這個問題,須要用到 unsafe.Pointer
。它表示任意類型且可尋址的指針值,能夠在不一樣的指針類型之間進行轉換(相似 C 語言的 void * 的用途)指針
其包含四種核心操做:code
在這一部分,重點看第一點、第二點。你再想一想怎麼修改 「錯誤示例」 讓它運行起來?
func main(){ num := 5 numPointer := &num flnum := (*float32)(unsafe.Pointer(numPointer)) fmt.Println(flnum) }
輸出結果:
0xc4200140b0
在上述代碼中,咱們小加改動。經過 unsafe.Pointer
的特性對該指針變量進行了修改,就能夠完成任意類型(*T)的指針轉換
須要注意的是,這時還沒法對變量進行操做或訪問。由於不知道該指針地址指向的東西具體是什麼類型。不知道是什麼類型,又如何進行解析呢。沒法解析也就天然沒法對其變動了
在上小節中,咱們對普通的指針變量進行了修改。那麼它是否能作更復雜一點的事呢?
type Num struct{ i string j int64 } func main(){ n := Num{i: "EDDYCJY", j: 1} nPointer := unsafe.Pointer(&n) niPointer := (*string)(unsafe.Pointer(nPointer)) *niPointer = "煎魚" njPointer := (*int64)(unsafe.Pointer(uintptr(nPointer) + unsafe.Offsetof(n.j))) *njPointer = 2 fmt.Printf("n.i: %s, n.j: %d", n.i, n.j) }
輸出結果:
n.i: 煎魚, n.j: 2
在剖析這段代碼作了什麼事以前,咱們須要瞭解結構體的一些基本概念:
再回來看看上述代碼,得出執行流程:
n.i
值:i
爲第一個成員變量。所以不須要進行偏移量計算,直接取出指針後轉換爲 Pointer
,再強制轉換爲字符串類型的指針值便可n.j
值:j
爲第二個成員變量。須要進行偏移量計算,才能夠對其內存地址進行修改。在進行了偏移運算後,當前地址已經指向第二個成員變量。接着重複轉換賦值便可須要注意的是,這裏使用了以下方法(來完成偏移計算的目標):
一、uintptr:uintptr
是 Go 的內置類型。返回無符號整數,可存儲一個完整的地址。後續經常使用於指針運算
type uintptr uintptr
二、unsafe.Offsetof:返回成員變量 x 在結構體當中的偏移量。更具體的講,就是返回結構體初始位置到 x 之間的字節數。須要注意的是入參 ArbitraryType
表示任意類型,並不是定義的 int
。它實際做用是一個佔位符
func Offsetof(x ArbitraryType) uintptr
在這一部分,其實就是巧用了 Pointer
的第3、第四點特性。這時候就已經能夠對變量進行操做了 😄
func main(){ n := Num{i: "EDDYCJY", j: 1} nPointer := unsafe.Pointer(&n) ... ptr := uintptr(nPointer) njPointer := (*int64)(unsafe.Pointer(ptr + unsafe.Offsetof(n.j))) ... }
這裏存在一個問題,uintptr
類型是不能存儲在臨時變量中的。由於從 GC 的角度來看,uintptr
類型的臨時變量只是一個無符號整數,並不知道它是一個指針地址
所以當知足必定條件後,ptr
這個臨時變量是可能被垃圾回收掉的,那麼接下來的內存操做,豈不成迷?
簡潔回顧兩個知識點。第一是 unsafe.Pointer
可讓你的變量在不一樣的指針類型轉來轉去,也就是表示爲任意可尋址的指針類型。第二是 uintptr
經常使用於與 unsafe.Pointer
打配合,用於作指針運算,巧妙地很
最後仍是那句,沒有特殊必要的話。是不建議使用 unsafe
標準庫,它並不安全。雖然它經常能讓你眼前一亮 👌