目錄
上一篇文章我們?cè)敿?xì)分析了 map 的底層實(shí)現(xiàn),如果你也跟著閱讀了源碼,那一定對(duì) unsafe.Pointer
不陌生,map 對(duì) key 進(jìn)行定位的時(shí)候,大量使用。
unsafe.Pointer
位于 unsafe 包
,這篇文章,我們來深入研究 unsafe 包。先說明一下,本文沒有之前那么長了,你可以比較輕松地讀完,這樣的時(shí)候不是太多。
上次發(fā)布文章的時(shí)候,包括代碼超過 5w 字,后臺(tái)編輯器的體驗(yàn)非常差,一度讓我懷疑人生。我之前說過,像 map 那樣的長文,估計(jì)能讀完的不超過 1 %
。像下面這幾位同學(xué)的評(píng)價(jià),并不多見。
個(gè)人認(rèn)為,學(xué)習(xí)本身并不是一件輕松愉快的事情,寓教于樂是個(gè)美好的愿望。想要深刻地領(lǐng)悟,就得付出別人看不見的努力。學(xué)習(xí)從來都不會(huì)是一件輕松的事情,枯燥是正常的。耐住性子,深入研究某個(gè)問題,讀書、看文章、寫博客都可以,浮躁時(shí)代做個(gè)專注的人!
指針類型
在正式介紹 unsafe 包之前,需要著重介紹 Go 語言中的指針類型。
我本科開始學(xué)編程的時(shí)候,第一門語言就是 C。之后又陸續(xù)學(xué)過 C++,Java,Python,這些語言都挺強(qiáng)大的,但是沒了 C 語言那么“單純”。直到我開始接觸 Go 語言,又找到了那種感覺。Go 語言的作者之一 Ken Thompson 也是 C 語言的作者。所以,Go 可以看作 C 系語言,它的很多特性都和 C 類似,指針就是其中之一。
然而,Go 語言的指針相比 C 的指針有很多限制。這當(dāng)然是為了安全考慮,要知道像 Java/Python 這些現(xiàn)代語言,生怕程序員出錯(cuò),哪有什么指針(這里指的是顯式的指針)?更別說像 C/C++ 還需要程序員自己清理“垃圾”。所以對(duì)于 Go 來說,有指針已經(jīng)很不錯(cuò)了,僅管它有很多限制。
為什么需要指針類型呢?參考文獻(xiàn) go101.org 里舉了這樣一個(gè)例子:
package main
import "fmt"
func double(x int) {
x += x
}
func main() {
var a = 3
double(a)
fmt.Println(a) // 3
}
非常簡單,我想在 double 函數(shù)里將 a 翻倍,但是例子中的函數(shù)卻做不到。為什么?因?yàn)?Go 語言的函數(shù)傳參都是值傳遞
。double 函數(shù)里的 x 只是實(shí)參 a 的一個(gè)拷貝,在函數(shù)內(nèi)部對(duì) x 的操作不能反饋到實(shí)參 a。
如果這時(shí),有一個(gè)指針就可以解決問題了!這也是我們常用的“伎倆”。
package main
import "fmt"
func double(x *int) {
*x += *x
x = nil
}
func main() {
var a = 3
double(&a)
fmt.Println(a) // 6
p := &a
double(p)
fmt.Println(a, p == nil) // 12 false
}
很常規(guī)的操作,不用多解釋。唯一可能有些疑惑的在這一句:
x = nil
這得稍微思考一下,才能得出這一行代碼根本不影響的結(jié)論。因?yàn)槭侵祩鬟f,所以 x 也只是對(duì) &a 的一個(gè)拷貝。
*x += *x
這一句把 x 指向的值(也就是 &a 指向的值,即變量 a)變?yōu)樵瓉淼?2 倍。但是對(duì) x 本身(一個(gè)指針)的操作卻不會(huì)影響外層的 a,所以 x = nil
掀不起任何大風(fēng)大浪。
下面的這張圖可以“自證清白”:
然而,相比于 C 語言中指針的靈活,Go 的指針多了一些限制。但這也算是 Go 的成功之處:既可以享受指針帶來的便利,又避免了指針的危險(xiǎn)性。
限制一:Go 的指針不能進(jìn)行數(shù)學(xué)運(yùn)算
。
來看一個(gè)簡單的例子:
a := 5
p := &a
p++
p = &a + 3
上面的代碼將不能通過編譯,會(huì)報(bào)編譯錯(cuò)誤:invalid operation
,也就是說不能對(duì)指針做數(shù)學(xué)運(yùn)算。
限制二:不同類型的指針不能相互轉(zhuǎn)換
。
例如下面這個(gè)簡短的例子:
func main() {
a := int(100)
var f *float64
f = &a
}
也會(huì)報(bào)編譯錯(cuò)誤:
cannot use &a (type *int) as type *float64 in assignment
關(guān)于兩個(gè)指針能否相互轉(zhuǎn)換,參考資料中 go 101 相關(guān)文章里寫得非常細(xì),這里我不想展開。個(gè)人認(rèn)為記住這些沒有什么意義,有完美主義的同學(xué)可以去閱讀原文。當(dāng)然我也有完美主義,但我有時(shí)會(huì)克制,嘿嘿。
限制三:不同類型的指針不能使用 == 或 != 比較
。
只有在兩個(gè)指針類型相同或者可以相互轉(zhuǎn)換的情況下,才可以對(duì)兩者進(jìn)行比較。另外,指針可以通過 ==
和 !=
直接和 nil
作比較。
限制四:不同類型的指針變量不能相互賦值
。
這一點(diǎn)同限制三。
什么是 unsafe
前面所說的指針是類型安全的,但它有很多限制。Go 還有非類型安全的指針,這就是 unsafe 包提供的 unsafe.Pointer。在某些情況下,它會(huì)使代碼更高效,當(dāng)然,也更危險(xiǎn)。
unsafe 包用于 Go 編譯器,在編譯階段使用。從名字就可以看出來,它是不安全的,官方并不建議使用。我在用 unsafe 包的時(shí)候會(huì)有一種不舒服的感覺,可能這也是語言設(shè)計(jì)者的意圖吧。
但是高階的 Gopher,怎么能不會(huì)使用 unsafe 包呢?它可以繞過 Go 語言的類型系統(tǒng),直接操作內(nèi)存。例如,一般我們不能操作一個(gè)結(jié)構(gòu)體的未導(dǎo)出成員,但是通過 unsafe 包就能做到。unsafe 包讓我可以直接讀寫內(nèi)存,還管你什么導(dǎo)出還是未導(dǎo)出。
為什么有 unsafe
Go 語言類型系統(tǒng)是為了安全和效率設(shè)計(jì)的,有時(shí),安全會(huì)導(dǎo)致效率低下。有了 unsafe 包,高階的程序員就可以利用它繞過類型系統(tǒng)的低效。因此,它就有了存在的意義,閱讀 Go 源碼,會(huì)發(fā)現(xiàn)有大量使用 unsafe 包的例子。
unsafe 實(shí)現(xiàn)原理
我們來看源碼:
type ArbitraryType int
type Pointer *ArbitraryType
從命名來看,Arbitrary
是任意的意思,也就是說 Pointer 可以指向任意類型,實(shí)際上它類似于 C 語言里的 void*
。
unsafe 包還有其他三個(gè)函數(shù):
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr
Sizeof
返回類型 x 所占據(jù)的字節(jié)數(shù),但不包含 x 所指向的內(nèi)容的大小。例如,對(duì)于一個(gè)指針,函數(shù)返回的大小為 8 字節(jié)(64位機(jī)上),一個(gè) slice 的大小則為 slice header 的大小。
Offsetof
返回結(jié)構(gòu)體成員在內(nèi)存中的位置離結(jié)構(gòu)體起始處的字節(jié)數(shù),所傳參數(shù)必須是結(jié)構(gòu)體的成員。
Alignof
返回 m,m 是指當(dāng)類型進(jìn)行內(nèi)存對(duì)齊時(shí),它分配到的內(nèi)存地址能整除 m。
注意到以上三個(gè)函數(shù)返回的結(jié)果都是 uintptr 類型,這和 unsafe.Pointer 可以相互轉(zhuǎn)換。三個(gè)函數(shù)都是在編譯期間執(zhí)行,它們的結(jié)果可以直接賦給 const 型變量
。另外,因?yàn)槿齻€(gè)函數(shù)執(zhí)行的結(jié)果和操作系統(tǒng)、編譯器相關(guān),所以是不可移植的。
綜上所述,unsafe 包提供了 2 點(diǎn)重要的能力:
- 任何類型的指針和 unsafe.Pointer 可以相互轉(zhuǎn)換。
- uintptr 類型和 unsafe.Pointer 可以相互轉(zhuǎn)換。
pointer 不能直接進(jìn)行數(shù)學(xué)運(yùn)算,但可以把它轉(zhuǎn)換成 uintptr,對(duì) uintptr 類型進(jìn)行數(shù)學(xué)運(yùn)算,再轉(zhuǎn)換成 pointer 類型。
// uintptr 是一個(gè)整數(shù)類型,它足夠大,可以存儲(chǔ)
type uintptr uintptr
還有一點(diǎn)要注意的是,uintptr 并沒有指針的語義,意思就是 uintptr 所指向的對(duì)象會(huì)被 gc 無情地回收。而 unsafe.Pointer 有指針語義,可以保護(hù)它所指向的對(duì)象在“有用”的時(shí)候不會(huì)被垃圾回收。
unsafe 包中的幾個(gè)函數(shù)都是在編譯期間執(zhí)行完畢,畢竟,編譯器對(duì)內(nèi)存分配這些操作“了然于胸”。在 /usr/local/go/src/cmd/compile/internal/gc/unsafe.go
路徑下,可以看到編譯期間 Go 對(duì) unsafe 包中函數(shù)的處理。
更深層的原理需要去研究編譯器的源碼,這里就不去深究了。我們重點(diǎn)關(guān)注它的用法,接著往下看。
unsafe 如何使用
獲取 slice 長度
通過前面關(guān)于 slice 的文章,我們知道了 slice header 的結(jié)構(gòu)體定義:
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指針
len int // 長度
cap int // 容量
}
調(diào)用 make 函數(shù)新建一個(gè) slice,底層調(diào)用的是 makeslice 函數(shù),返回的是 slice 結(jié)構(gòu)體:
func makeslice(et *_type, len, cap int) slice
因此我們可以通過 unsafe.Pointer 和 uintptr 進(jìn)行轉(zhuǎn)換,得到 slice 的字段值。
func main() {
s := make([]int, 9, 20)
var Len = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8)))
fmt.Println(Len, len(s)) // 9 9
var Cap = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16)))
fmt.Println(Cap, cap(s)) // 20 20
}
Len,cap 的轉(zhuǎn)換流程如下:
Len: &s => pointer => uintptr => pointer => *int => int
Cap: &s => pointer => uintptr => pointer => *int => int
獲取 map 長度
再來看一下上篇文章我們講到的 map:
type hmap struct {
count int
flags uint8
B uint8
noverflow uint16
hash0 uint32
buckets unsafe.Pointer
oldbuckets unsafe.Pointer
nevacuate uintptr
extra *mapextra
}
和 slice 不同的是,makemap 函數(shù)返回的是 hmap 的指針,注意是指針:
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap
我們依然能通過 unsafe.Pointer 和 uintptr 進(jìn)行轉(zhuǎn)換,得到 hamp 字段的值,只不過,現(xiàn)在 count 變成二級(jí)指針了:
func main() {
mp := make(map[string]int)
mp["qcrao"] = 100
mp["stefno"] = 18
count := **(**int)(unsafe.Pointer(&mp))
fmt.Println(count, len(mp)) // 2 2
}
count 的轉(zhuǎn)換過程:
&mp => pointer => **int => int
map 源碼中的應(yīng)用
在 map 源碼中,mapaccess1、mapassign、mapdelete 函數(shù)中,需要定位 key 的位置,會(huì)先對(duì) key 做哈希運(yùn)算。
例如:
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + (hash&m)*uintptr(t.bucketsize)))
h.buckets
是一個(gè) unsafe.Pointer
,將它轉(zhuǎn)換成 uintptr
,然后加上 (hash&m)*uintptr(t.bucketsize)
,二者相加的結(jié)果再次轉(zhuǎn)換成 unsafe.Pointer
,最后,轉(zhuǎn)換成 bmap 指針
,得到 key 所落入的 bucket 位置。如果不熟悉這個(gè)公式,可以看看上一篇文章,淺顯易懂。
上面舉的例子相對(duì)簡單,來看一個(gè)關(guān)于賦值的更難一點(diǎn)的例子:
// store new key/value at insert position
if t.indirectkey {
kmem := newobject(t.key)
*(*unsafe.Pointer)(insertk) = kmem
insertk = kmem
}
if t.indirectvalue {
vmem := newobject(t.elem)
*(*unsafe.Pointer)(val) = vmem
}
typedmemmove(t.key, insertk, key)
這段代碼是在找到了 key 要插入的位置后,進(jìn)行“賦值”操作。insertk 和 val 分別表示 key 和 value 所要“放置”的地址。如果 t.indirectkey 為真,說明 bucket 中存儲(chǔ)的是 key 的指針,因此需要將 insertk 看成指針的指針
,這樣才能將 bucket 中的相應(yīng)位置的值設(shè)置成指向真實(shí) key 的地址值,也就是說 key 存放的是指針。
下面這張圖展示了設(shè)置 key 的全部操作:
obj 是真實(shí)的 key 存放的地方。第 4 號(hào)圖,obj 表示執(zhí)行完 typedmemmove
函數(shù)后,被成功賦值。
Offsetof 獲取成員偏移量
對(duì)于一個(gè)結(jié)構(gòu)體,通過 offset 函數(shù)可以獲取結(jié)構(gòu)體成員的偏移量,進(jìn)而獲取成員的地址,讀寫該地址的內(nèi)存,就可以達(dá)到改變成員值的目的。
這里有一個(gè)內(nèi)存分配相關(guān)的事實(shí):結(jié)構(gòu)體會(huì)被分配一塊連續(xù)的內(nèi)存,結(jié)構(gòu)體的地址也代表了第一個(gè)成員的地址。
我們來看一個(gè)例子:
package main
import (
"fmt"
"unsafe"
)
type Programmer struct {
name string
language string
}
func main() {
p := Programmer{"stefno", "go"}
fmt.Println(p)
name := (*string)(unsafe.Pointer(&p))
*name = "qcrao"
lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.language)))
*lang = "Golang"
fmt.Println(p)
}
運(yùn)行代碼,輸出:
{stefno go}
{qcrao Golang}
name 是結(jié)構(gòu)體的第一個(gè)成員,因此可以直接將 &p 解析成 *string。這一點(diǎn),在前面獲取 map 的 count 成員時(shí),用的是同樣的原理。
對(duì)于結(jié)構(gòu)體的私有成員,現(xiàn)在有辦法可以通過 unsafe.Pointer 改變它的值了。
我把 Programmer 結(jié)構(gòu)體升級(jí),多加一個(gè)字段:
type Programmer struct {
name string
age int
language string
}
并且放在其他包,這樣在 main 函數(shù)中,它的三個(gè)字段都是私有成員變量,不能直接修改。但我通過 unsafe.Sizeof() 函數(shù)可以獲取成員大小,進(jìn)而計(jì)算出成員的地址,直接修改內(nèi)存。
func main() {
p := Programmer{"stefno", 18, "go"}
fmt.Println(p)
lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(int(0)) + unsafe.Sizeof(string(""))))
*lang = "Golang"
fmt.Println(p)
}
輸出:
{stefno 18 go}
{stefno 18 Golang}
string 和 slice 的相互轉(zhuǎn)換
這是一個(gè)非常精典的例子。實(shí)現(xiàn)字符串和 bytes 切片之間的轉(zhuǎn)換,要求是 zero-copy
。想一下,一般的做法,都需要遍歷字符串或 bytes 切片,再挨個(gè)賦值。
完成這個(gè)任務(wù),我們需要了解 slice 和 string 的底層數(shù)據(jù)結(jié)構(gòu):
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
上面是反射包下的結(jié)構(gòu)體,路徑:src/reflect/value.go。只需要共享底層 []byte 數(shù)組就可以實(shí)現(xiàn) zero-copy
。
func string2bytes(s string) []byte {
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: stringHeader.Data,
Len: stringHeader.Len,
Cap: stringHeader.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}
func bytes2string(b []byte) string{
sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := reflect.StringHeader{
Data: sliceHeader.Data,
Len: sliceHeader.Len,
}
return *(*string)(unsafe.Pointer(&sh))
}
代碼比較簡單,不作詳細(xì)解釋。通過構(gòu)造 slice header 和 string header,來完成 string 和 byte slice 之間的轉(zhuǎn)換。
總結(jié)
unsafe 包繞過了 Go 的類型系統(tǒng),達(dá)到直接操作內(nèi)存的目的,使用它有一定的風(fēng)險(xiǎn)性。但是在某些場(chǎng)景下,使用 unsafe 包提供的函數(shù)會(huì)提升代碼的效率,Go 源碼中也是大量使用 unsafe 包。
unsafe 包定義了 Pointer 和三個(gè)函數(shù):
type ArbitraryType int
type Pointer *ArbitraryType
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr
通過三個(gè)函數(shù)可以獲取變量的大小、偏移、對(duì)齊等信息。
uintptr 可以和 unsafe.Pointer 進(jìn)行相互轉(zhuǎn)換,uintptr 可以進(jìn)行數(shù)學(xué)運(yùn)算。這樣,通過 uintptr 和 unsafe.Pointer 的結(jié)合就解決了 Go 指針不能進(jìn)行數(shù)學(xué)運(yùn)算的限制。
通過 unsafe 相關(guān)函數(shù),可以獲取結(jié)構(gòu)體私有成員的地址,進(jìn)而對(duì)其做進(jìn)一步的讀寫操作,突破 Go 的類型安全限制。關(guān)于 unsafe 包,我們更多關(guān)注它的用法。
順便說一句,unsafe 包用多了之后,也不覺得它的名字有多么地不“美觀”了。相反,因?yàn)槭褂昧斯俜讲⒉惶岢臇|西,反而覺得有點(diǎn)酷炫。這就是叛逆的感覺吧。
最后,點(diǎn)擊閱讀原文,你將參與見證一個(gè)千星項(xiàng)目的成長,你值得擁有!