Go 空結構體引發(fā)的大型打臉現場
背景
上周讀者問了我一道題,覺得挺有意義的,就在這里分享一下,我們先來看一下這個題:
type User struct {
}
func FPrint(u User) {
fmt.Printf("FPrint %p\n", &u)
}
func main() {
u := User{}
FPrint(u)
fmt.Printf("main: %p\n", &u)
}
// 運行結果
FPrint 0x118eff0
main: 0x118eff0
看了運行結果,大多數朋友應該和我一樣,一臉懵逼?Go語言不是只有值傳遞嘛?之前我還寫過一篇關于"Go語言參數傳遞是傳值還是傳引用嗎?",已經得出明確的結論,Go語言的確是只有值傳遞,這不是打臉了嘛。。。
既然已經出現了這樣的結果,那么就要給出一個合理的解釋,不要再讓氣氛尷尬下去,于是我給出了我的猜想,如下:
猜想一:這是一個 bug猜想二:結構體的特殊特性導致的
猜想一有點天馬行空的感覺,暫時也無法驗證,所以我們先來驗證猜想二,請開始我的表演,都坐下,我要裝逼了。。。。
驗證猜想二:結構體的特殊特性導致的
上面的那道題中傳參是一個空結構體,如果改成一個帶字段的結構體會是什么樣呢?我們來看一下:
type UserIsEmpty struct {
}
type UserHasField struct {
Age uint64 `json:"age"`
}
func FPrint(uIsEmpty UserIsEmpty, uHasField UserHasField) {
fmt.Printf("FPrint uIsEmpty:%p uHasField:%p\n", &uIsEmpty, &uHasField)
}
func main() {
uIsEmpty := UserIsEmpty{}
uHasField := UserHasField{
Age: 10,
}
FPrint(uIsEmpty, uHasField)
fmt.Printf("main: uIsEmpty:%p uHasField:%p\n", &uIsEmpty, &uHasField)
}
// 運行結果:
FPrint uIsEmpty:0x118fff0 uHasField:0xc0000ba008
main: uIsEmpty:0x118fff0 uHasField:0xc0000ba000
從結果我們可以看出來,帶字段的結構體確實是值傳遞,那么就證明空結構體有貓膩,有進展了,帶著這個線索,我們來看一看這段代碼的匯編部分,執(zhí)行go tool compile -N -l -S test.go,可以得到匯編部分,截取重要部分:

從結果上我們看到有調用runtime.newobject(SB)來進行分配內存,順著這個在runtme/malloc.go中找到了他的實現:
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.size, typ, true)
}
newobject()中主要是調用了mallocgc()方法,在這里我找到了答案。因為mallocgc()代碼比較長,這里我截取關鍵部分:
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
if gcphase == _GCmarktermination {
throw("mallocgc called with gcphase == _GCmarktermination")
}
if size == 0 {
return unsafe.Pointer(&zerobase)
}
..........
}
如果 size 為 0 的時候,統(tǒng)一返回的都是全局變量 zerobase 的地址。到這里可能還會有一些伙伴有疑惑,這個跟上面的題有什么關系?那是因為你還不知道一個知識點:正常struct是占用一小塊內存的,并且結構體的大小是要經過邊界,長度的對齊的,但是“空結構體”是不占內存的,size為0?,F在一切都可以說的清了,總結原因:
因為空結構體是不占用內存的,所以
size為0,在內存分配時,size為0會統(tǒng)一返回zerobase的地址,所以空結構體在進行參數傳遞時,發(fā)生值拷貝后地址都是一樣的,才造成了這個質疑Go不是值傳遞的假象。
空結構體特性延伸
既然說到了空結構體,就在這里補充一個關于空結構體的知識點:空結構體做為結構體內置字段時是否進行內存對齊。
先來看一個例子:
func main(){
fmt.Println(unsafe.Sizeof(Test1{}))
fmt.Println(unsafe.Sizeof(Test2{}))
fmt.Println(unsafe.Sizeof(Test3{}))
}
type Test1 struct {
s struct{}
n byte
m byte
}
type Test2 struct {
n byte
s struct{}
c byte
}
type Test3 struct {
b byte
s struct{}
}
//運行結果
2
2
2
根據運行結果我們可以得出結論:
空結構體在結構體中的前面和中間時,是不占用空間的,但是當空結構體放到結構體中的最后時,會進行特殊填充,struct { } 作為最后一個字段,會被填充對齊到前一個字段的大小,地址偏移對齊規(guī)則不變;
總結
最后做一個全文總結吧:
空結構體也是一個結構體,不過他的 size為0,所有的空結構體內存分配都是同一個地址,都是zerobase的地址;空結構體作為內嵌字段時要注意放置的順序,當作為最后一個字段時會進行特殊填充,會被填充對齊到前一個字段的大小,地址偏移對齊規(guī)則不變;
推薦閱讀
