Golang 學習之 unsafe

【導讀】go 語言的 unsafe 標準庫有什麼妙用?如何高效利用 unsafe 能力進行操作?本文對 unsafe 標準庫做了介紹。

unsafe

雖然我們程序中引入 unsafe import "unsafe" 像是引入其他使用 go 實現的包一樣,unsafe 包下的功能是不是通過 go 代碼實現的,而是通過編譯器實現的。

unsafe 中的功能暴露了 Go 底層的實現細節,雖然 Go 是跨平臺的,但是每個平臺上 Go 底層實現都不一樣,這樣就造成在不同平臺上 unsafe 的表現可能有所不同,而且 unsafe 不保證向後兼容。unsafe 包廣泛被和操作系統交互的低級包中,如 runtime、os、syscall 和 net。

一般程序不需要使用 unsafe

舉一個例子體現 unsafe 的奇怪:

type ArbitraryType int

func Sizeof(x ArbitraryType) uintptr

如果站在 Go 語法上說,unsafe.Sizeof() 不可能接收任意類型的參數,但是事實上 unsafe.Sizeof() 可以接收任何類型的參數,所以說這個非常奇怪。編譯器做了手腳

slice 中結構中不是真的存儲了一個數組的指針,而是一個 unsafe.Pointer:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

int 類型的長度都是跟當前操作系統的位數相關的,比如在 32 位系統上爲 32 位,在 64 系統上爲 64 位。

操作處理數據最小的單位不是 bit,也不是 byte 而是一個字(不是字節)。那麼一個字的長度是多少呢?32 位操作系統爲 32 位 4 字節,64 位操作系統爲 64 位 8 字節。

我電腦爲 64 位操作系統:

fmt.Println(unsafe.Sizeof(int(0)))  // 8

結構體中變量編排

如果結構體成員的類型是不同的,那麼將相同類型的成員定義在一起可以節省內存空間。以下三個結構體擁有相同的成員,但是第一個定義比其他兩個定義要多佔內存。

fmt.Println(unsafe.Sizeof(struct {
        bool; float64; int16
    }{}))       // 24
    fmt.Println(unsafe.Sizeof(struct {
        float64; int16; bool
    }{}))       // 16
    fmt.Println(unsafe.Sizeof(struct {
        bool; int16; float64
    }{}))       // 16
var x struct {
    a bool
    b int16
    c []int
}

其內存佈局如下:

Sizeof(x) = 32      Alignof(x) = 8
Sizeof(x.a) = 1     Alignof(x.a) = 1    Offsetof(x.a) = 0
Sizeof(x.b) = 2     Alignof(x.b) = 2    Offsetof(x.b) = 2
Sizeof(x.c) = 24    Alignof(x.c) = 8    Offsetof(x.c) = 8

Alignof 查看的對齊方式。x.a 是一個字節一個字節地對齊,x.b 是兩個字節兩個字節地對齊,x.c 是八個字節八個字節對齊。

Offsetof 查看變量在從結構體開頭的偏移量。Offsetof(x.a) = 0 表明 x 和 a 的起始地址相同。

Sizeof、Alignof、Offsetof 三個方法是安全的,我們可以通過他們來查看某個結構體中變量的大小、對齊和排列等信息。

unsafe.Pointer

unsafe.Pointer 是一個特殊的指針,能夠指向任何類型的變量地址。但是無法直接使用 unsafe.Pointer 指針對變量進行操作或訪問,因爲還不知道指向地址的具體類型,因爲只有知道了具體類型後才知道如何解析裏面的數據。如,01101000 一個字節可以解析爲'h' 或者是 104。

我們也可以將指針的內容強制解析爲某些類型,或者直接對其數據進行更改(這是不安全的)。下面例子中,我們將浮點數的內容當做 int64 來解析,然後又將整形數目寫入到本來是浮點數的內存中,最後當做浮點數來解析:

f := 1.0
pf := &f
pi := (*int64)(unsafe.Pointer(pf))
fmt.Printf("%d\n", *pi)     // 4607182418800017408
*pi = 0
fmt.Printf("%g\n", f)       // 0

這樣的代碼可讀性必然差。

還可以訪問任意本程序中的任意內存地址?

是的,uintptr 和 unsafe.pointer 之間進行轉換,而 unsafe.Pointer 可以轉化爲任意類型的指針。但是 uintptr 傳遞的不都是合法的內存地址,這樣做會破壞類型系統。但是還是來個 demo 看看:

var x struct {
    a bool
    b int16
    c []int
    }
pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
fmt.Printf("%v %v\n", pb, &x.b)   // 0xc42000a062 0xc42000a062
*pb = 1
fmt.Println(x.b)    // 1

上面我們中規中矩採用的是 x 的地址再加偏移量得到的 b 的地址,要是我們隨便讀取一個地址呢?

ptr := (*int16)(unsafe.Pointer(uintptr(0xc42000a0)))
fmt.Printf("%v %b\n", ptr, *ptr)

輸出:

unexpected fault address 0xc42000a0
fatal error: fault
[signal SIGSEGV: segmentation violation code=0x1 addr=0xc42000a0 pc=0x48988b]

Ooops,說了不是所有地址都是合法的內存地址。

還有一種很隱晦的錯誤:

tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 1

如果進行 GC 後,變量的位置可能已經發生了移動,這時候地址可能已經不是之前那個地址了。當進行垃圾回收時變量移動,指針(unsafe.Pointer、*T)的值也跟隨這變量的地址改變而改變,但是上面例子 tmp 是一個 uintptr 類型變量,GC 不會進行指針同步,緩存失效。goroutine 的連續棧增長時也會導致類似錯誤。

類似的錯誤:

ptr := uintptr(unsafe.Pointer(new(T)))

變量 new(T) 可能在創建後馬上被 GC 回收,因爲 GC 檢測不到有任何指針指向該內存。uintptr 不是指針

兩個地址相同的變量一定相等嗎?

如果你還記得上面那個圖片,那麼你就知道不是的。不信的話運行下面的程序:

var x struct {
    a bool
    b int16
    c []int
}
fmt.Printf("%p %p\n"&x, &x.a)

轉自:

zhuanlan.zhihu.com/p/34288219

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ubFHlNFIYpO2-8sdLic-HA