用 Go struct 不能犯的一個低級錯誤!

微信搜索【 腦子進煎魚了】關注這一隻爆肝煎魚。本文 GitHub github.com/eddycjy/blog 已收錄,有個人系列文章、資料和開源 Go 圖書。

你們好,我是煎魚。git

前段時間我分享了 《手撕 Go 面試官:Go 結構體是否能夠比較,爲何?》的文章,把基本 Go struct 的比較依據研究了一番。這不,最近有一位讀者,遇到了一個關於 struct 的新問題,不得解。github

你們一塊兒來看看,建議你們在看到代碼例子後先思考一下答案,再往下看。golang

獨立思考很重要。面試

疑惑的例子

其給出的例子一以下:算法

type People struct {}

func main() {
 a := &People{}
 b := &People{}
 fmt.Println(a == b)
}

你認爲輸出結果是什麼呢?bash

輸出結果是:false。微信

再稍加改造一下,例子二以下:flex

type People struct {}

func main() {
 a := &People{}
 b := &People{}
 fmt.Printf("%p\n", a)
 fmt.Printf("%p\n", b)
 fmt.Println(a == b)
}

輸出結果是:true。優化

他的問題是 "爲何第一個返回 false 第二個返回 true,是什麼緣由致使的ui

煎魚進一步的精簡這個例子,獲得最小示例:

func main() {
    a := new(struct{})
    b := new(struct{})
    println(a, b, a == b)

    c := new(struct{})
    d := new(struct{})
    fmt.Println(c, d)
    println(c, d, c == d)
}

輸出結果:

// a, b; a == b
0xc00005cf57 0xc00005cf57 false

// c, d
&{} &{}
// c, d, c == d
0x118c370 0x118c370 true

第一段代碼的結果是 false,第二段的結果是 true,且能夠看到內存地址指向的徹底同樣,也就是排除了輸出後變量內存指向改變致使的緣由。

進一步來看,彷佛是 fmt.Print 方法致使的,但一個標準庫裏的輸出方法,會致使這種奇怪的問題?

問題剖析

若是以前有被這個 「坑」 過,或有看過源碼的同窗。可能可以快速的意識到,致使這個輸出是逃逸分析所致的結果。

咱們對例子進行逃逸分析:

// 源代碼結構
$ cat -n main.go
     5    func main() {
     6        a := new(struct{})
     7        b := new(struct{})
     8        println(a, b, a == b)
     9    
    10        c := new(struct{})
    11        d := new(struct{})
    12        fmt.Println(c, d)
    13        println(c, d, c == d)
    14    }

// 進行逃逸分析
$ go run -gcflags="-m -l" main.go
# command-line-arguments
./main.go:6:10: a does not escape
./main.go:7:10: b does not escape
./main.go:10:10: c escapes to heap
./main.go:11:10: d escapes to heap
./main.go:12:13: ... argument does not escape

經過分析可得知變量 a, b 均是分配在棧中,而變量 c, d 分配在堆中。

其關鍵緣由是由於調用了 fmt.Println 方法,該方法內部是涉及到大量的反射相關方法的調用,會形成逃逸行爲,也就是分配到堆上。

爲何逃逸後相等

關注第一個細節,就是 「爲何逃逸後,兩個空 struct 會是相等的?」。

這裏主要與 Go runtime 的一個優化細節有關,以下:

// runtime/malloc.go
var zerobase uintptr

變量 zerobase 是全部 0 字節分配的基礎地址。更進一步來說,就是空(0字節)的在進行了逃逸分析後,往堆分配的都會指向 zerobase 這一個地址。

因此空 struct 在逃逸後本質上指向了 zerobase,其二者比較就是相等的,返回了 true。

爲何沒逃逸不相等

關注第二個細節,就是 「爲何沒逃逸前,兩個空 struct 比較不相等?」。

Go spec

從 Go spec 來看,這是 Go 團隊刻意而爲之的設計,不但願你們依賴這一個來作判斷依據。以下:

This is an intentional language choice to give implementations flexibility in how they handle pointers to zero-sized objects. If every pointer to a zero-sized object were required to be different, then each allocation of a zero-sized object would have to allocate at least one byte. If every pointer to a zero-sized object were required to be the same, it would be different to handle taking the address of a zero-sized field within a larger struct.

還說了一句很經典的,細品:

Pointers to distinct zero-size variables may or may not be equal.

另外空 struct 在實際使用中的場景是比較少的,常見的是:

  • 設置 context,傳遞時做爲 key 時用到。
  • 設置空 struct 業務場景中臨時用到。

但業務場景的狀況下,也大多數會隨着業務發展而不斷改變,假設有個遠古時代的 Go 代碼,依賴了空 struct 的直接判斷,豈不是事故上身?

不可直接依賴

所以 Go 團隊這番操做,與 Go map 的隨機性一模一樣,避免你們對這類邏輯的直接依賴,是值得思考的。

而在沒逃逸的場景下,兩個空 struct 的比較動做,你覺得是真的在比較。實際上已經在代碼優化階段被直接優化掉,轉爲了 false。

所以,雖然在代碼上看上去是 == 在作比較,實際上結果是 a == b 時就直接轉爲了 false,比都不須要比了。

你說妙不?

沒逃逸讓他相等

既然咱們知道了他是在代碼優化階段被優化的,那麼相對的,知道了原理的咱們也能夠藉助在 go 編譯運行時的 gcflags 指令,讓他不優化。

在運行前面的例子時,執行 -gcflags="-N -l" 指令:

$ go run -gcflags="-N -l" main.go 
0xc000092f06 0xc000092f06 true
&{} &{}
0x118c370 0x118c370 true

你看,兩個比較的結果都是 true 了。

總結

在今天這篇文章中,咱們針對 Go 語言中的空結構體(struct)的比較場景進行了進一步的補全。通過這兩篇文章的洗禮,你會更好的理解 Go 結構體爲何叫既可比較又不可比較了。

而空結構比較的奇妙,主要緣由以下:

  • 若逃逸到堆上,空結構體則默認分配的是 runtime.zerobase 變量,是專門用於分配到堆上的 0 字節基礎地址。所以兩個空結構體,都是 runtime.zerobase,一比較固然就是 true 了。
  • 若沒有發生逃逸,也就分配到棧上。在 Go 編譯器的代碼優化階段,會對其進行優化,直接返回 false。並非傳統意義上的,真的去比較了。

不會有人拿來出面試題,不會吧,爲何 Go 結構體說可比較又不可比較?

如有任何疑問歡迎評論區反饋和交流,最好的關係是互相成就,各位的點贊就是煎魚創做的最大動力,感謝支持。

文章持續更新,能夠微信搜【腦子進煎魚了】閱讀,回覆【 000】有我準備的一線大廠面試算法題解和資料;本文 GitHub github.com/eddycjy/blog 已收錄,歡迎 Star 催更。

參考

  • 歐神的微信交流
  • 曹大的一個空 struct 的「坑」
相關文章
相關標籤/搜索