Go1.20 將會修改全域性變數的初始化順序。梅開二度,繼續打破 Go1 相容性承諾!

煎魚發表於2022-12-19

大家好,我是煎魚。

Go1.20 釋出在即,大家都關注了一些大頭的功能特性,例如:PGO、Arean 等。都沒有那麼的常接觸到。

實質上本次新版本還修復了在全域性變數初始化方面的順序,來自《cmd/compile: global variable initialization done in unexpected order》,這是個挺有趣的問題。

神奇案例

從案例展開,假設在同一個 package 下有 2 個檔案,分別是:f1.go 和 f2.go,包含了不同的包全域性變數宣告和程式碼。

檔案 f1.go。程式碼如下:

package main    
   
var A int = 3    
var B int = A + 1    
var C int = A

檔案 f2.go。程式碼如下:

package main    
   
import "fmt"    
                     
var D = f()      
   
func f() int {    
  A = 1    
  return 1    
}    
   
func main() {    
  fmt.Println(A, B, C)    
}  

問題來了。

如果執行 go run f1.go f2.go,會輸出什麼結果?

執行結果如下:

1 4 3

你答對了嗎?再仔細想想。

如果執行 go run f2.go f1.go,會輸出什麼結果?

執行結果如下:

1 2 3

這只是 run 的檔案先後順序不一樣了,咋就連輸出的結果都不一樣了?

輸出結果到底誰對誰錯,還是說都錯了,正確的是什麼?

Go 規範定義

我們要知道正確輸出的結果是什麼,還得是看 Go 語言規範《The Go Programming Language Specification》說了算。

sepc

在規範中的包初始化(Package initialization)章節中明確指出:"在一個包中,包級別的變數初始化是逐步進行的,每一步都會選擇宣告順序中最早的變數,它不依賴於未初始化的變數。"

更完整和準確的闡述:

  • 如果包級變數尚未初始化並且沒有初始化表示式或其初始化表示式不依賴於未初始化的變數,則認為包級變數已準備好進行初始化。
  • 初始化透過重複初始化宣告順序中最早並準備初始化的下一個包級變數來進行,直到沒有變數準備好進行初始化。

在瞭解了理論知識後,我們再結合官方例子看看,加強實踐的補全。

例子 1。程式碼如下:

var x = a
var a, b = f()

在初始化變數 x 之前,變數 a 和 b 會一起初始化(在同一步驟中)。

例子 2。程式碼如下:

var (
    a = c + b  // == 9
    b = f()    // == 4
    c = f()    // == 5
    d = 3      // == 5 after initialization has finished
)

func f() int {
    d++
    return d
}

初始化順序是:d, b, c, a。

案例哪裡有問題

在解讀了背景和規範後,再次回顧文章剛開始的案例。

檔案 f1.go。程式碼如下:

package main    
   
var A int = 3    
var B int = A + 1    
var C int = A

檔案 f2.go。程式碼如下:

package main    
   
import "fmt"    
                     
var D = f()      
   
func f() int {    
  A = 1    
  return 1    
}    
   
func main() {    
  fmt.Println(A, B, C)    
}  

第一種,執行 go run f1.go f2.go,輸出:1 4 3。

第二種,執行 go run f2.go f1.go,輸出:1 2 3.

如果按照規範來,分析程式變數初始化順序和應該輸出的結果。如下:

  • A < B < C < D:發生在你編譯專案時,執行命令先把 f1.go 傳給編譯器,然後再傳 f2.go。在這種情況下,輸出結果是 1 4 3。
  • A < D < B < C:發生在先將 f2.go 傳給編譯器時。在這種情況下,預期輸出是 1 2 1。然而,實際的輸出是 1 2 3。

問題出在第二種情況,我們嘗試改一下寫法,變成如下程式碼:

package main    
   
import "fmt"    
   
var A int = initA()    
var B int = initB()    
var C int = initC()    
     
func initA() int {    
  fmt.Println("Init A")    
  return 3    
}    
     
func initB() int {    
  fmt.Println("Init B")    
  return A + 1    
}    
 
func initC() int {    
  fmt.Println("Init C")    
  return A    
} 

輸出結果:

Init A
Init B
Init C
1 2 1

預期結果就一致了。

這是有 BUG!與 Go 規範定義的不一致。

修復時間

目前這個問題已經明確是 Go 編譯/執行時的 BUG,並且這個問題已經存在了很久,將計劃在 Go1.20 中修復。

不過由於不知道是否會影響使用者,因此 Go 官方將會更多的關注社群反饋。當然,這個確實是 BUG,會修。也為此認為值得打破 Go1 相容性的原則。

總結

今天這篇文章我們介紹了 Go 一直以來存在的一個 Go 編譯/執行時的 BUG,會導致 Go 程式的全域性變數會與 Go 規範本身定義的不一致,將預計會在 Go1.20 修復。

這也是 Go 打破 Go1 相容性承諾的一個案例。值得我們關注。

文章持續更新,可以微信搜【腦子進煎魚了】閱讀,本文 GitHub github.com/eddycjy/blog 已收錄,學習 Go 語言可以看 Go 學習地圖和路線,歡迎 Star 催更。

Go 圖書系列

推薦閱讀

相關文章