這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
《Go語言實戰》讀書筆記,未完待續,歡迎掃碼關注公眾號flysnow_org或者網站http://www.flysnow.org/,第一時間看後續筆記。覺得有協助的話,順手分享到朋友圈吧,感謝支援。
Go語言在設計的時候,為了編寫方便、效率高以及降低複雜度,被設計成為一門強型別的靜態語言。強型別意味著一旦定義了,它的類型就不能改變了;靜態意味著類型檢查在運行前就做了。
同時為了安全的考慮,Go語言是允許兩個指標類型進行轉換的。
指標類型轉換
我們一般使用*T作為一個指標類型,表示一個指向類型T變數的指標。為了安全的考慮,兩個不同的指標類型不能相互轉換,比如*int不能轉為*float64。
12345678 |
func main() {i:= 10ip:=&ivar fp *float64 = (*float64)(ip)fmt.Println(fp)} |
以上代碼我們在編譯的時候,會提示cannot convert ip (type *int) to type *float64,也就是不能進行強制轉型。那如果我們還是需要進行轉換怎麼做呢?這就需要我們使用unsafe包裡的Pointer了,下面我們先看看unsafe.Pointer是什麼,然後再介紹如何轉換。
Pointer
unsafe.Pointer是一種特殊意義的指標,它可以包含任意類型的地址,有點類似於C語言裡的void*指標,全能型的。
12345678910 |
func main() {i:= 10ip:=&ivar fp *float64 = (*float64)(unsafe.Pointer(ip))*fp = *fp * 3fmt.Println(i)} |
以上樣本,我們可以把*int轉為*float64,並且我們嘗試了對新的*float64進行操作,列印輸出i,就會發現i的址同樣被改變。
以上這個例子沒有任何實際的意義,但是我們說明了,通過unsafe.Pointer這個萬能的指標,我們可以在*T之間做任何轉換。
123 |
type ArbitraryType inttype Pointer *ArbitraryType |
可以看到unsafe.Pointer其實就是一個*int,一個通用型的指標。
我們看下關於unsafe.Pointer的4個規則。
- 任何指標都可以轉換為
unsafe.Pointer
unsafe.Pointer可以轉換為任何指標
uintptr可以轉換為unsafe.Pointer
unsafe.Pointer可以轉換為uintptr
前面兩個規則我們剛剛已經示範了,主要用於*T1和*T2之間的轉換,那麼最後兩個規則是做什麼的呢?我們都知道*T是不能計算位移量的,也不能進行計算,但是uintptr可以,所以我們可以把指標轉為uintptr再進行便宜計算,這樣我們就可以訪問特定的記憶體了,達到對不同的記憶體讀寫的目的。
下面我們以通過指標位移修改Struct結構體內的欄位為例,來示範uintptr的用法。
1234567891011121314151617 |
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 = 20fmt.Println(*u)}type user struct {name stringage 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就可以操作了。
這裡我們可以看到,我們第二個位移的運算式非常長,但是也千萬不要把他們分段,不能像下面這樣。
123 |
temp:=uintptr(unsafe.Pointer(u))+unsafe.Offsetof(u.age)pAge:=(*int)(unsafe.Pointer(temp))*pAge = 20 |
邏輯上看,以上代碼不會有什麼問題,但是這裡會牽涉到GC,如果我們的這些臨時變數被GC,那麼導致的記憶體操作就錯了,我們最終操作的,就不知道是哪塊記憶體了,會引起莫名其妙的問題。
小結
unsafe是不安全的,所以我們應該儘可能少的使用它,比如記憶體的操縱,這是繞過Go本身設計的安全機制的,不當的操作,可能會破壞一塊記憶體,而且這種問題非常不好定位。
當然必須的時候我們可以使用它,比如底層類型相同的數組之間的轉換;比如使用sync/atomic包中的一些函數時;還有訪問Struct的私人欄位時;該用還是要用,不過一定要慎之又慎。
還有,整個unsafe包都是用於Go編譯器的,不用運行時,在我們編譯的時候,Go編譯器已經把他們都處理了。
《Go語言實戰》讀書筆記,未完待續,歡迎掃碼關注公眾號flysnow_org或者網站http://www.flysnow.org/,第一時間看後續筆記。覺得有協助的話,順手分享到朋友圈吧,感謝支援。