包的初始化——golang中包級(jí)別變量謹(jǐn)慎使用其他變量賦值

在一次開發(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)給編譯器。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容