Go unsafe 包探究
unsafe
包的作用有兩個:
- 實現任意不同型別指標之間的轉換;
- 實現指標運算(偏移)操作;
包介面比較簡單,包括 3 個函式:
- func Alignof(x ArbitraryType) uintptr 變數對齊;
- func Offsetof(x ArbitraryType) uintptr 計算結構體(struct)內屬性值的偏移位元組大小,即相對結構體起始地址的大小;
- func Sizeof(x ArbitraryType) uintptr 型別變數自身佔用位元組大小,注意,不包括變數引用的地址;
unsafe 函式都是在編譯時計算返回結果的,所以,可以直接用於常量賦值,也要注意,儘量不要將執行時變數型別(例如 slice)作為這些函式的引數出入,可能會導致非預期的結果。
包括 2 種型別:
- type ArbitraryType 佔位符,實際上表示任意的變數型別;
- type Pointer 指向任意型別的指標型別。類似 C 中 void * 型別。
unsafe.Pointer
是本包的精華,也是被使用最多的功能點。Pointer 允許程式(開發者)跳脫 Go 的型別系統,(通過指標轉換與運算)讀寫任意記憶體,所以要小心使用。
Pointer 總結下來就兩個特性,也是實現前面說的目標(作用)的基礎:
- Pointer 能與任意型別的指標值互相轉換;
- Pointer 能與 uintpter 相互轉換;
uintptr
是無符號整型,被用來存放指標值(地址)。unsafe.Pointer
+ uintptr
就能實現指標偏移計算了。因為 uintptr
變數存放的是某個變數的地址,因此,uintpter
變數值對應的記憶體地址塊(對應的變數)可能會被 GC 回收掉。unsafe.Pointer
本質上就是指標,該型別變數指向的記憶體塊則不會被回收,因此,應該使用 unsafe.Pointer
型別變數來保持變數地址不被回收。
// 不安全的使用
z := uintptr(unsafe.Pointer(&xx))
//todo ...
fmt.Println(z)
//正確使用
sp:=safe.Pointer(&xx)
z = uintptr(sp)
//todo ...
fmt.Println(z)
安全的使用場景
前面說過,使用 Pointer 必須要非常小心才行,官方定義了 6 種安全有效的使用場景。使用 go vet
工具可以檢測出不符合這些場景的呼叫。
- 將指標
*T1
轉化成*T2
如果 T2 大於 T1 變數型別的記憶體佔用,並且兩者共享等效的記憶體佈局,則該轉換允許將一種型別資料解釋成為另一種型別。例如:int64 與 float64。
- 將 Pointer 轉成 uintptr,但是,不能轉回到 Pointer
與 Pointer 不同,uintptr 儲存的地址指向的變數是可以被 GC 回收的。
可以使用 runtime.KeepAlive
函式避免變數被 GC。
- 將 Pointer 轉成 uintptr,用於偏移運算,將計算結果轉回成 Pointer
通常用於訪問結構體或者陣列等:
// equivalent to f := unsafe.Pointer(&s.f)
f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f))
// equivalent to e := unsafe.Pointer(&x[i])
e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + i*unsafe.Sizeof(x[0]))
特別注意:因為上面說的原因,uintptr 不能放在臨時變數內,所以下面這樣分開使用也是無效的:
u := uintptr(p)
p = unsafe.Pointer(u + offset)
另外,做地址偏移的時候,要注意越界的問題,比如:
a = []int{0,1,2,3}
p := unsafe.Pointer(uintptr(unsafe.Pointer(&a)) + len(a) * unsafe.Sizeif(a[0]))
此時,變數 p 指向的地址是未知的,可能會出現不宣告,直接偷偷的讀寫未知記憶體地址的情況,對系統執行穩定性影響很大。
- 使用函式
syscall.Syscall
時,將 Pointer 值轉成 uintptr
syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))
- 將函式 reflect.Value.Pointer 或 reflect.Value.UnsafeAddr 返回值,從 uintptr 轉成 Pointer,最終轉成具體的型別值
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))
- 將 reflect.SliceHeader 或 reflect.StringHeader 的資料欄位(Data)轉成 Pointer,或者從 Pointer 轉成 Data 欄位
Data
欄位返回的也是 uintptr:
var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
hdr.Data = uintptr(unsafe.Pointer(p)) // case 6 (this case)
hdr.Len = n
reflect.SliceHeader
的使用:
package main
import "fmt"
import "unsafe"
import "reflect"
import "runtime"
func main() {
bs := []byte("Golang")
var pa *[2]byte // an array pointer
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
pa = (*[2]byte)(unsafe.Pointer(hdr.Data))
runtime.KeepAlive(&bs)
fmt.Printf("%s\n", pa) // &Go
pa[1] = 'a'
fmt.Printf("%s\n", bs) // Galang
}
如果最後一行的 Printf 不存在的話,runtime.KeepAlive
的呼叫是必須的。
另外,最好不要像下面這樣,直接從 StringHeader 或 StringHeader 直接建立物件:
// Assume p points to a sequence of byte and
// n is the number of bytes in the sequence.
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(new([5]byte)))
// Now the just allocated byte array has lose all
// references and it can be garbage collected now.
hdr.Len = 5
s := *(*string)(unsafe.Pointer(&hdr))
小結
在有些場景下,使用 unsafe.Poniter
可以幫助我們寫出高效的程式碼,例如在 sync/atomic
包內的使用。而且一些底層或 C 呼叫,必須要用到 Poniter。
unsafe 包用於有經驗的開發者繞過 Go 型別系統的安全性限制,一定要深入理解上面的六種使用場景,謹慎使用,否則很容易引起嚴重的記憶體問題,有經驗的開發者都知道,這類問題通常是很難定位的,對系統的穩定性影響很大。
參考
Go 101 : https://go101.org/article/unsafe.html
更多技術文章分享
相關文章
- Go unsafe包Go
- 深入探索Go語言的unsafe包,揭秘它的黑科技和應用場景!Go
- Go高階特性 16 | 非型別安全:unsafeGo型別
- Unsafe
- 詳解 Go 團隊不建議用的 unsafe.PointerGo
- Go:context包GoContext
- Go strconv包Go
- Unsafe原子性
- __unsafe_unretainedAI
- go reflect包中abi.goGo
- 有點不安全卻又一亮的 Go unsafe.PointerGo
- Go標準包-http包serverGoHTTPServer
- golang如何使用指標靈活操作記憶體?unsafe包原理解析Golang指標記憶體
- Java中的UnsafeJava
- Java 的 Unsafe 類Java
- 詳解Unsafe類
- go 閉包函式Go函式
- Go 操作kafka包saramaGoKafka
- Go之time包用法Go
- Go | 閉包的使用Go
- Go標準包——net/rpc包的使用GoRPC
- Jdk之Unsafe總結JDK
- Refused to set unsafe header "cookie"HeaderCookie
- Java安全之Unsafe類Java
- go語言處理TCP拆包/粘包GoTCP
- Go 閉包的實現Go
- 初識go的tomb包Go
- GO語言————6.8 閉包Go
- Go module 本地導包方式Go
- Go語言之包(package)管理GoPackage
- Go TCP 粘包問題GoTCP
- 聊聊Go裡面的閉包Go
- Go標準包—http clientGoHTTPclient
- Java併發包中執行緒池ThreadPoolExecutor原理探究Java執行緒thread
- 【Go】Go語言學習筆記-3-包Go筆記
- Go語言基於go module方式管理包(package)GoPackage
- 用“揹包”去理解Go語言中的閉包Go
- golang unsafe.Pointer與uintptrGolangUI