Go 1.22引入的包級變量初始化次序問題
共 8562字,需瀏覽 18分鐘
·
2024-04-07 12:01
細(xì)心的朋友可能已經(jīng)注意到,從春節(jié)后,我的博客就“停更”了!實際上,這一情況部分是因為工作上的事務(wù)繁忙,另一部分則是因為我將工作之外的閑暇時間更多地投入到一本即將于今年中下旬出版的書的撰寫了:在之前的積累基礎(chǔ)上,我花了兩個多月的時間完成了初稿。
當(dāng)然,我也深切地懷念博客寫作所帶來的樂趣和與讀者的互動。正巧,今天一位學(xué)員在《Go語言第一課》專欄[1]留言給了我一個恢復(fù)下筆的機會。借此,我也準(zhǔn)備恢復(fù)一下博客寫作的節(jié)奏。
另外預(yù)告一下:我和我的技術(shù)團隊合作翻譯的一本Go語言入門書最早也將于2024年4月份上市,敬請期待!
在《Go語言第一課》專欄[2]的第8講[3]中,我曾系統(tǒng)講解了Go包的初始化次序,以及Go包內(nèi)包級變量、常量、init函數(shù)等的初始化次序。講這些的初衷就是希望Go初學(xué)者能先了解一下Go程序的執(zhí)行次序,這樣在后續(xù)閱讀和理解Go代碼的時候,就好比擁有了“通往寶藏的地圖”,可以直接沿著Go代碼執(zhí)行次序這張“地圖”去閱讀和理解Go代碼,而不會在龐大的代碼庫中迷失了。
相對于早期的Go版本,Go包的初始化次序在Go 1.21版本[4]開始會有所變化,這個可以看我的《Go 1.21中值得關(guān)注的幾個變化》[5]一文了解詳情。
不過除了Go包的初始化次序得以明確之外,Go在1.22版本中的包級變量初始化次序也發(fā)生了一些“變化”,但Go 1.22的Release Note[6]壓根沒提到Go包內(nèi)的變量初始化次序會有變化。究竟這些變化是有意為之,還是由于代碼變更而引入的新問題呢?我們還得從近期《Go語言第一課》專欄[7]的一位讀者提出的問題講起!
1. Go 1.22的輸出結(jié)果與專欄文章中不同!
原專欄中的代碼較多,為方便起見我又寫了一段簡化版的代碼,可以等價地反映問題。下面的代碼用于演示包級變量、常量和init函數(shù)的初始化次序:
// initorder.go
package main
import (
"fmt"
)
var (
v0 = constInitCheck()
v1 = variableInit("v1")
v2 = variableInit("v2")
)
const (
c1 = "c1"
c2 = "c2"
)
func constInitCheck() string {
if c1 != "" {
fmt.Println("main: const c1 has been initialized")
}
if c1 != "" {
fmt.Println("main: const c2 has been initialized")
}
return ""
}
func variableInit(name string) string {
fmt.Printf("main: var %s has been initialized\n", name)
return name
}
func init() {
fmt.Println("main: first init func invoked")
}
func init() {
fmt.Println("main: second init func invoked")
}
func main() {
// do nothing
}
使用Go 1.22版本之前的版本,比如Go 1.21版本[8],運行該程序的輸出結(jié)果如下:
$go run initorder.go
main: const c1 has been initialized
main: const c2 has been initialized
main: var v1 has been initialized
main: var v2 has been initialized
main: first init func invoked
main: second init func invoked
這個輸出結(jié)果也是專欄文章中的輸出結(jié)果,即包級元素的初始化順序是:常量 -> 變量 -> init函數(shù)。三個變量的初始化次序是v0 -> v1 -> v2。
但專欄的一位讀者在使用最新Go 1.22版本運行上述程序后,卻提出了如下問題:
總結(jié)一下這個問題的兩個關(guān)鍵點如下:
-
Go 1.22版本運行上述程序的輸出結(jié)果與文章中的結(jié)果不一致 -
將const聲明block搬移到var聲明block的前面后,使用Go 1.22版本的輸出結(jié)果與文章中的一致
我們先來復(fù)現(xiàn)一下問題。我使用Go 1.22.0運行上面的initorder.go,得到下面結(jié)果:
$go run main.go
main: var v1 has been initialized
main: var v2 has been initialized
main: const c1 has been initialized
main: const c2 has been initialized
main: first init func invoked
main: second init func invoked
該輸出結(jié)果確如讀者所說,與文中的輸出順序不一致了,變量的初始化次序變?yōu)榱藇1 -> v2 -> v0。這會讓很多讀者誤以為包內(nèi)元素的初始化次序變成了“變量 -> 常量 -> init函數(shù)”。是否真的如此了呢?我們下面來初步分析一下。
2. 原因初步分析
Go語言規(guī)范[9]中對包內(nèi)變量初始化次序的說明[10]是這樣的(截至2024.03):
Within a package, package-level variable initialization proceeds stepwise, with each step selecting the variable earliest in declaration order which has no dependencies on uninitialized variables. More precisely, a package-level variable is considered ready for initialization if it is not yet initialized and either has no initialization expression or its initialization expression has no dependencies on uninitialized variables. Initialization proceeds by repeatedly initializing the next package-level variable that is earliest in declaration order and ready for initialization, until there are no variables ready for initialization. Multiple variables on the left-hand side of a variable declaration initialized by single (multi-valued) expression on the right-hand side are initialized together: If any of the variables on the left-hand side is initialized, all those variables are initialized in the same step. For the purpose of package initialization, blank variables are treated like any other variables in declarations.
粗略翻譯后大致意思如下:
在包內(nèi),包級變量初始化逐步進行,每一步都會選擇聲明順序中最早的且不依賴于未初始化變量的那個變量。更準(zhǔn)確地說,如果包級變量尚未初始化并且沒有初始化表達式或其初始化表達式不依賴于未初始化的變量,則認(rèn)為該變量具備初始化條件。通過重復(fù)初始化聲明順序中最早且具備初始化條件的下一個包級變量來進行初始化,直到?jīng)]有具備初始化條件的變量為止。由右側(cè)單個(多值)表達式初始化的變量聲明左側(cè)的多個變量會一起初始化:如果左側(cè)的任何變量被初始化,則所有這些變量都會被初始化在同一步驟中。出于包初始化的目的,空變量也被視為與聲明中的任何其他變量一樣。
按照Go語言規(guī)范的描述,我們來理論推導(dǎo)一下v0、v1和v2的初始化次序:
var (
v0 = constInitCheck()
v1 = variableInit("v1")
v2 = variableInit("v2")
)
-
第一輪:待初始化的包級變量集合{v0, v1, v2}。在這一輪,我們按聲明順序逐一看一下這三個變量。
v0未初始化,其聲明語句的右側(cè)有初始化表達式(initialization expression),且這個初始化表達式式(constInitCheck)不依賴未初始化的變量(僅僅依賴兩個常量c1和c2),因此按照Spec描述,v0具備初始化條件,會先進行初始化,于是constInitCheck會被調(diào)用。
-
第二輪:待初始化的包級變量集合{v1, v2}。
按聲明順序,先看v1。和v0一樣,其聲明語句的右側(cè)有初始化表達式,且這個初始化表達式式(variableInit)不依賴未初始化的變量,因此按照Spec描述,v1具備初始化條件,會進行初始化,于是variableInit會被調(diào)用。
-
第三輪:待初始化的包級變量集合{v2}。
這個沒啥可推導(dǎo)的了,初始化v2就是了!
這樣,包級變量的聲明次序就應(yīng)該是v0 -> v1 -> v2。這個理論推導(dǎo)結(jié)果顯然與Go 1.22版之前的輸出結(jié)果是一致的。但與Go 1.22版本的輸出結(jié)果有悖。
那么Go 1.22版本為什么沒有將v0作為第一個具備初始化條件的變量對其進行初始化呢?v0有初始化表達式constInitCheck,該函數(shù)沒有依賴任何未初始化的包級變量,但該函數(shù)內(nèi)部依賴了兩個常量c1和c2:
func constInitCheck() string {
if c1 != "" {
fmt.Println("main: const c1 has been initialized")
}
if c1 != "" {
fmt.Println("main: const c2 has been initialized")
}
return ""
}
我們大膽地猜測一下:Go 1.22版本將c1和c2當(dāng)成了“未初始化的變量”了!還記得讀者問題的第二個關(guān)鍵點嗎:“將const聲明block搬移到var聲明block的前面后,使用Go 1.22版本的輸出結(jié)果便與文章中的一致”。按照Go 1.22的邏輯,將常量聲明放到前面后,按順序常量先被初始化了。這樣到v0時,v0具備初始化的條件就成立了,于是v0就可以先被初始化了。
3. “一波三折”的issue
為了證實上述推測,我在github.com/golang/go提了issue 66575[11],并對上述問題做了闡述,不過該issue被Go團隊的年輕成員Sean Liao[12]“閃電”關(guān)閉了。
好在幾個小時后,Go大神Keith Randall[13]看到了這個issue,并支持了我的猜測!他還閃電般地找出了導(dǎo)致Go 1.22版本出現(xiàn)此問題的commit[14],并給出了fix方案:cmd/compile: put constants before variables in initialization order[15]。fix方案的思路就是將所有常量的初始化放到變量之前。
該fix merge到主干[16]后,Gobot自動關(guān)閉了該issue。
但嚴(yán)謹(jǐn)?shù)腒eith Randall隨后reopen了該issue,并圈了Go語言之父的Robert Griesemer[17],希望后者確定一下是否需要更新一下Go spec。
目前該issue已經(jīng)被加入Go 1.23 milestone[18],并會在Go 1.23 fix。
《Go語言第一課》專欄: http://gk.link/a/10AVZ
[2]《Go語言第一課》專欄: http://gk.link/a/10AVZ
[3]第8講: https://time.geekbang.org/column/article/432021
[4]Go 1.21版本: https://go.dev/doc/go1.21
[5]《Go 1.21中值得關(guān)注的幾個變化》: https://tonybai.com/2023/08/20/some-changes-in-go-1-21
[6]Go 1.22的Release Note: https://go.dev/doc/go1.22
[7]《Go語言第一課》專欄: http://gk.link/a/10AVZ
[8]Go 1.21版本: https://tonybai.com/2023/08/20/some-changes-in-go-1-21/
[9]Go語言規(guī)范: https://go.dev/ref/spec
[10]包內(nèi)變量初始化次序的說明: https://go.dev/ref/spec#Package_initialization
[11]issue 66575: https://github.com/golang/go/issues/66575
[12]Sean Liao: https://github.com/seankhliao
[13]Keith Randall: https://github.com/randall77
[14]導(dǎo)致Go 1.22版本出現(xiàn)此問題的commit: https://go-review.googlesource.com/c/go/+/517617
[15]fix方案:cmd/compile: put constants before variables in initialization order: https://go-review.googlesource.com/c/go/+/575075
[16]fix merge到主干: https://github.com/golang/go/commit/8f618c1f5329bd81912f8f776cb8bf028c750687
[17]Robert Griesemer: https://github.com/griesemer
[18]Go 1.23 milestone: https://github.com/golang/go/milestone/212
推薦閱讀:
Go區(qū)不大,創(chuàng)造神話,科目三殺進來了
想要了解Go更多內(nèi)容,歡迎掃描下方??關(guān)注公眾號,掃描 [實戰(zhàn)群]二維碼 ,即可進群和我們交流~
- 掃碼即可加入實戰(zhàn)群 -
