在一次開發(fā)過(guò)程中,想要將程序運(yùn)行環(huán)境的變量作為緩存的key值作為區(qū)分,因此在聲明全局變量時(shí)使用了其他變量賦值。通過(guò)如下測(cè)試代碼簡(jiǎn)化具體邏輯。
var a string
var b = fmt.Sprintf("prefix_%s",a)
func init(){
a = "test"
}
func TestPrint(t *testing.T) {
fmt.Println(a)
fmt.Println(b)
}
當(dāng)我們執(zhí)行測(cè)試程序時(shí),詭異的事情發(fā)生了。
=== RUN TestPrint
test
prefix_
--- PASS: TestPrint (0.00s)
PASS
變量b在初始化時(shí),a變量并未如預(yù)想的結(jié)果被初始化。后續(xù)我們通過(guò)查閱資料發(fā)現(xiàn)是由于包初始化順序?qū)е碌?,?jiǎn)單來(lái)說(shuō),對(duì)于測(cè)試代碼初始化順序?yàn)椋?/p>
- 變量
- init()
- main()/test()
更詳細(xì)的內(nèi)容參考Package initialization。這部分內(nèi)容總結(jié)如下:
包初始化
在一個(gè)包內(nèi),包級(jí)別變量的初始化是逐步進(jìn)行的,初始化過(guò)程按照變量聲明順序中最早,同時(shí)不依賴其他未初始化的變量的順序進(jìn)行。
更準(zhǔn)確地說(shuō),如果包級(jí)別變量的初始化不是通過(guò)初始化表達(dá)式或其初始化過(guò)程不依賴于未初始化的變量,則認(rèn)為它已準(zhǔn)備好進(jìn)行初始化。 初始化按照變量的聲明順序進(jìn)行。
如果一個(gè)或多個(gè)初始化周期依賴于某些尚未被初始化的變量,則這些處理無(wú)效。這部可以參考上述測(cè)試代碼。b依賴a完后初始化,但是b初始化時(shí),a還未被初始化(var a string 只是a被聲明,真正的初始化是在init()),因此b初始化時(shí)對(duì)于a的依賴未生效,或者說(shuō)此時(shí)a為空,b = “prefix_”。
如果左側(cè)多個(gè)變量通過(guò)右側(cè)單個(gè)(多個(gè))表達(dá)式初始化,那么所有變量將在同一步驟被初始化。
var x = a
var a, b = f() // a和b將在x被初始化之前同時(shí)初始化
包初始化時(shí),空白變量與聲明中的任何其他變量一樣,也會(huì)被處理。
在多個(gè)文件中變量的聲明順序由文件呈現(xiàn)給編譯器的順序決定:第一個(gè)文件中聲明的變量一定在第二個(gè)文件中聲明的任意變量之前初始化。
依賴分析不依賴于變量的實(shí)際值,只依賴于源文件中對(duì)它們的詞法引用,并進(jìn)行傳遞分析。 例如,如果變量 x 的初始化表達(dá)式引用一個(gè)函數(shù),其函數(shù)體引用變量 y,則 x 取決于 y。 例如:
var (
a = c + b // == 9
b = f() // == 4
c = f() // == 5
d = 3 // == 5 after initialization has finished
)
func f() int {
d++
return d
}
通過(guò)實(shí)驗(yàn)結(jié)果可以發(fā)現(xiàn),初始化順序是d, b, c, a。請(qǐng)注意,初始化表達(dá)式中子表達(dá)式的順序無(wú)關(guān)緊要: a = c + b和a = b + c在本示例中產(chǎn)生相同的初始化順序。
每個(gè)包在初始化時(shí)都執(zhí)行依賴分析;僅考慮對(duì)當(dāng)前包中聲明的變量、函數(shù)和(非接口)方法的引用。如果變量之間存在其他隱藏的數(shù)據(jù)依賴關(guān)系,則不指定這些變量之間的初始化順序。例如:
var x = I(T{}).ab() // x 存在對(duì) a,b的隱藏依賴
var _ = sideEffect() // 不依賴x, a,或b
var a = b
var b = 42
type I interface { ab() []int }
type T struct{}
func (T) ab() []int { return []int{a, b} }
Package initialization中說(shuō)變量a將在b之后被初始化,但是x是在b之前,還是b,a之間或者a之后初始化,以及sideEffect()被調(diào)用的順序(在 x 初始化之前或之后)是未被指定的。
但是我通過(guò)如下實(shí)驗(yàn)發(fā)現(xiàn)真實(shí)的情況和文檔有些出入,具體的:對(duì)上述測(cè)試代碼進(jìn)行修改。
var x = I(T{}).ab() // x 存在對(duì) a,b的隱藏依賴
var y = sideEffect() // 不依賴x, a,或b
var a = b
var b = 42
type I interface { ab() []int }
type T struct{}
func (T) ab() []int {
time.Sleep(1*time.Second)
fmt.Println(a,b)
return []int{a, b}
}
func sideEffect() int{
fmt.Println("init side")
return 1
}
func TestPrint(t *testing.T) {
fmt.Println(a)
fmt.Println(b)
fmt.Println(x)
fmt.Println(y)
}
實(shí)驗(yàn)結(jié)果如下:
42 42
init side
=== RUN TestPrint
42
42
[42 42]
1
--- PASS: TestPrint (0.00s)
PASS
實(shí)驗(yàn)結(jié)果表明,初始化順序?yàn)椋篵,a,x,y。猜測(cè)應(yīng)該是依賴分析過(guò)程隱藏的依賴關(guān)系有被分析到。因此按照真實(shí)的依賴和聲明關(guān)系進(jìn)行初始化??紤]到是不是文檔版本太舊,源碼已經(jīng)進(jìn)行了優(yōu)化。但是文檔日期為Version of Feb 10, 2021。我使用的golang版本為go version go1.15 darwin/amd64,文檔出錯(cuò)的概率基本為0。于是再次優(yōu)化了測(cè)試代碼:
var x = I(T{}).ab() // x 存在對(duì) a,b的隱藏依賴
var y = sideEffect() // 不依賴x, a,或b
var a = f()
var b = 42
type I interface { ab() []int }
type T struct{}
func sideEffect() int{
fmt.Println("init side")
return 1
}
func f() int{
fmt.Println("init a")
return 42
}
func (T) ab() []int {
fmt.Println("init x")
return []int{a, b}
}
func TestPrint(t *testing.T) {
fmt.Println(x)
fmt.Println(y)
fmt.Println(a)
fmt.Println(b)
}
實(shí)驗(yàn)結(jié)果為:
init x
init side
init a
=== RUN TestPrint
[0 42]
1
42
42
--- PASS: TestPrint (0.00s)
PASS
由于x初始化時(shí),b并未被初始化,因此實(shí)驗(yàn)結(jié)果證明了,golang無(wú)法分析出這種基于接口的隱藏依賴關(guān)系。
變量也可以使用init方法進(jìn)行初始化。
func init() { … }
每個(gè)包中甚至一個(gè)原文件中也可以定義多個(gè)這樣的函數(shù)。init標(biāo)識(shí)符只能用于聲明init函數(shù),因此init函數(shù)不能從程序的任何地方被調(diào)用。
一個(gè)沒有imports的包通過(guò)按照init函數(shù)在源文件中出現(xiàn)的順序?yàn)槿肿兞糠峙涑跏蓟?。如果包中存在多個(gè)源文件,則按照展示給編譯器的文件順序初始化變量。如果有包導(dǎo)入,則在初始化包本身之前初始化導(dǎo)入的包。如果多個(gè)包導(dǎo)入一個(gè)包,導(dǎo)入的包只會(huì)被初始化一次。包的導(dǎo)入,需要確保不會(huì)有循環(huán)初始化依賴,否則將編譯錯(cuò)誤。
包初始化、變量初始化和 init 函數(shù)的調(diào)用會(huì)發(fā)生在同一個(gè) goroutine 中,該協(xié)程將逐個(gè)對(duì)包進(jìn)行初始化。一個(gè) init 函數(shù)可能會(huì)啟動(dòng)其他 goroutines,這些 goroutines 可以與初始化代碼同時(shí)運(yùn)行。然而,初始化總是對(duì) init 函數(shù)進(jìn)行排序:在前一個(gè)函數(shù)返回之前,它不會(huì)調(diào)用下一個(gè)函數(shù)。
為了確??芍噩F(xiàn)的初始化行為,鼓勵(lì)構(gòu)建系統(tǒng)以詞法文件名順序?qū)儆谕话亩鄠€(gè)文件呈現(xiàn)給編譯器。