Golang: unsafe 包真的不安全嗎?
在 Go 中,一般不建議使用 unsafe 包,因爲它「不安全」。本文探討這個問題。(由 Go 語言中文網公衆號注)
unsafe 包詳解
在烏克蘭的利沃夫舉行的 Lviv Golang community event[1] 中,我發表了一個關於 unsafe
包的演講,這個演講中我嘗試回答了標題中提到的問題:unsafe
包究竟有多 unsafe。
從 unsafe
包的名字就能感受到 Go 研發團隊的警告:使用這個包的代價將是巨大的。我覺得這個包名起的非常巧妙,它完美地符合了 《Effective Go》中對包名的所有建議。在使用 unsafe 包的時候,我們應當嚴格遵循 Go 研發團隊的文檔和建議。這個包的官方概述就只有簡單的一段話:
unsafe 包裏面包含了一些能讓你踐踏 Go 語言的類型安全特性的操作。golang.org[2]
附帶一段簡單的警告:
引用 unsafe 包可能會導致你代碼的不具備可移植性,並且不再受到 Go 1 兼容性規約的保護。golang.org[3]
函數功能的描述看起來非常的抽象,我們來瞅一眼這些 "unsafe" 的操作:
func Alignof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Sizeof(x ArbitraryType) uintptr
type ArbitraryType
type Pointer
其中有一個 ArbitraryType
類型:
只是爲了文檔記錄的目的而存在,實際上它沒有參與到 unsafe 包的實現。這個類型代表了任意的 Go 語言表達式。
所以實際上 unsafe 包就只包含三個函數和一個類型,既然就這麼點東西,那我們試着把這個包全部過一遍。現在我們手頭上已經有了 Go 研發團隊給我的文檔和源碼,下一步要怎麼做?這時候不妨重溫一句名人名言:
多說無益,放碼過來 —— Linus Torvalds[4]
好,既然如此我們就直接看源碼吧……
Gif
神奇的事情發生了——這個 unsafe 包壓根就沒有源碼 [5] 呀。(Go 語言中文網公衆號注:builtin 也沒有源碼)它有函數的簽名和類型定義,但是沒有實現的代碼:無論是 Go 還是彙編的代碼都沒有。之所以會出現這個情況,是因爲 unsafe 包的功能需要在層次更低的編譯器層面實現,所以這個包其實是內置在編譯器裏面實現的,這個 .go 文件只是爲了達到文檔記錄的目的。所以我在上文反覆強調要嚴格遵循 Go 研發團隊的文檔和建議,因爲你也只能看到這些文檔。廢話不多說,先來看看 Sizeof
函數吧。
func Sizeof(x ArbitraryType) uintptr
函數接受某個變量,然後返回 uintptr
類型的結果。這個函數的名字可以看出,這個函數返回某個變量的大小。爲了理解方便,請允許我用幾個圖示來可視化一下這些概念。衆所周知,我們的 Go 程序需要內存來完成各種功能,其中就包含使用內存來保存變量。下面我將用這些標籤來表示內存:
🎁 - 1 個字節的內存
📦 - 1 個字節的被佔用的內存
🥡 - 1 個字節的被佔用但實際沒有作用的內存(後面會詳細解釋這個)
⬆️ - 指向內存地址的指針
下面我使用這些標籤來展示這個結構的內存佈局:
type X struct {
n1 int16
n2 int16
}
它在內存中的佈局是這樣的
Sizeof memory usage
X
結構體有兩個字段,其中每一個都佔 2 個字節,所以整個結構體佔用 size(n1
) + size(n2
) + size(X
) = 2 + 2 + 0 = 4。顯然,下面語句是成立的:
unsafe.Sizeof(X) == 4 // true
func Offsetof(x ArbitraryType) uintptr
這個函數就有點難度了,函數簽名和上面的函數是同樣的,但是它返回的是 offset(偏移值)。我再次使用標籤來解釋這個機制——還是用剛纔的 X
結構體,還是同樣的兩個字段:
type X struct {
n1 int16
n2 int16
}
現在我們已經知道他在內存裏面是怎樣佈局的了,這一次我們來看看每個字段各佔多少個字節,內存分配的情況如下圖:
Offsetof memory usage
不難猜到,內存的佈局是這樣的:第一個字段 X.n1
佔了前 2 個字節,而第二個字段 X.n2
佔了接下來的 2 個字節。所以下面兩個語句都是成立的:
unsafe.Offsetof(X.n1) == 0 // true
unsafe.Offsetof(X.n2) == 2 // true
func Alignof(x ArbitraryType) uintptr
這個函數是最好玩的一個,因爲要透徹瞭解這個函數,你需要了解 alignment(數據結構對齊)[6] 是怎麼回事。簡單來說,它讓數據在內存中以某種的佈局來存放,使該數據的讀取能夠更加的快速。這個接收一個變量作爲參數,並返回這個變量的對齊字節。爲了更加直觀,我們需要修改一下上面的例子:
type X struct {
n1 int8
n2 int16
}
可以看到現在 n1
的類型變成了 int8
,這會有什麼變化嗎,我們先看看 Sizeof
, 因爲 n1
只佔 1 個字節了,所以合理地推測,X
結構體的大小會變成 3,因爲:size(X
) = size(n1
) + size(n2
) = 1 + 2 = 3。但是現實真的如此嗎 ?
……
不是的,因爲 alignment 的緣故,X
結構體在內存的結構如下:
Alignof memory usage
由於 alignment 機制的要求,n2
的內存起始地址應該是自身大小的整數倍,也就是說它的起始地址只能是 0、2、4、6、8 等偶數,所以 n2
的起始地址沒有緊接着 n1
後面,而是空出了 1 個字節。最後導致結構體 X
的大小是 4 而不是 3。機智的讀者可能會想到:n1
和 n2
換個位置會怎樣呢?這樣一來,n2
的起始地址是 0,而 n1
的其實地址是 2,這麼一來結構體 X
的大小就變成 3 了吧?答案是…… 不對的。原因還是因爲 alignment,因爲 alignment 除了要求字段的其實地址應該是自身大小的整數倍,還要求整個結構體的大小,是結構體中最大的字段的大小的整數倍,這使得結構體可以由多個內存塊組成,其中每個內存塊的大小都等於最大的字段的大小。我們可以利用這個知識來減少結構體的內存佔用。考察以下代碼:
type First struct {
a int8
b int64
c int8
}
type Second struct {
a int8
c int8
b int64
}
fmt.Println("Big brain time: ", unsafe.Sizeof(First{}) == unsafe.Sizeof(Second{}))
上面兩個結構體大小不同,是因爲 First
結構體由三個大小爲 8 字節的內存塊組成:Sizeof(First.a) + 7 個空閒的字節 + Sizeof(First.b) + Sizeof(First.c) + 7 個空閒的字節 = 24 字節
。而 Second
結構體只包含 2 個 大小爲 8 字節的內存塊:Sizeof(Second.a) + Sizeof(Second.b) + 6 個空閒的字節 + Sizeof(Second.b) = 16 字節
。下次你定義結構體的時候可以用上這個小知識🙂。
下面的代碼片段總結了上述三個函數的用法:
var x struct {
a int64
b bool
c string
}
fmt.Println("Size of x: ", unsafe.Sizeof(x))
fmt.Println("Size of x.c: ", unsafe.Sizeof(x.c))
fmt.Println("Alignment of x.a: ", unsafe.Alignof(x.a))
fmt.Println("Alignment of x.b: ", unsafe.Alignof(x.b))
fmt.Println("Alignment of x.c: ", unsafe.Alignof(x.c))
fmt.Println("\nOffset of x.a: ", unsafe.Offsetof(x.a))
fmt.Println("Offset of x.b: ", unsafe.Offsetof(x.b))
fmt.Println("Offset of x.c: ", unsafe.Offsetof(x.c))
上述的三個方法都是在編譯期 [7] 執行的,這意味着只要它們在編譯器沒有報錯,在運行時不會有問題發生。但是我們的下一位嘉賓 unsafe.Pointer
可就沒那麼好惹了,它有可能會發生 運行時的錯誤)。我將會在本文的第二部分詳細介紹 unsafe.Pointer
以及使用它的過程中容易出現的問題。
via: https://www.dnahurnyi.com/is-unsafe-...unsafe-pt.-1/
作者:Denys Nahurnyi[8] 譯者:Alex-liutao[9] 校對:@unknwon[10]
本文由 GCTT[11] 原創編譯,Go 中文網 [12] 榮譽推出。轉載請聯繫我們授權!
參考資料
[1]
Lviv Golang community event: https://www.facebook.com/events/470065893928934/482981832637340/?notif_t=admin_plan_mall_activity¬if_id=1580732874088578
[2]
golang.org: https://golang.org/pkg/unsafe/#pkg-overview
[3]
golang.org: https://golang.org/pkg/unsafe/#pkg-overview
[4]
Linus Torvalds: https://lkml.org/lkml/2000/8/25/132
[5]
沒有源碼: https://golang.org/src/unsafe/unsafe.go
[6]
alignment(數據結構對齊): https://zh.wikipedia.org/wiki / 數據結構對齊
[7]
編譯期: https://en.wikipedia.org/wiki/Compile_time
[8]
Denys Nahurnyi: https://www.dnahurnyi.com/
[9]
Alex-liutao: https://github.com/Aelx-liutao
[10]
@unknwon: https://github.com/unknwon
[11]
GCTT: https://github.com/studygolang/GCTT
[12]
Go 中文網: https://studygolang.com/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/rIqkKNUecvnZ6gadThf4gg