一文帶你了解【Go】初始化函數(shù)
環(huán)境
- OS : Ubuntu 20.04.2 LTS; x86_64
- Go : go version go1.16.2 linux/amd64
包初始化
初始化函數(shù)與其他普通函數(shù)一樣,都隸屬于定義它的包(package),以下統(tǒng)稱為當(dāng)前包。
一般來講,一個包初始化過程分三步:
- 初始化當(dāng)前包依賴的所有包,包括依賴包的依賴包。
- 初始化當(dāng)前包所有具有初始值的全局變量。
- 執(zhí)行當(dāng)前包的所有初始化函數(shù)。
關(guān)于這個過程,本文會一一詳細(xì)介紹。
基本定義
在Golang中有一類特殊的初始化函數(shù),其定義格式如下:
- package pkg
- func init() {
- // to do sth
- }
初始化函數(shù)一個特殊之處是:其在可執(zhí)行程序的main入口函數(shù)執(zhí)行之前自動執(zhí)行,而且不可被直接調(diào)用!
重復(fù)聲明
初始化函數(shù)第二個特殊之處是:在同一個包下,可以重復(fù)定義多次。
普通函數(shù)在同一個包下不可以重名,否則變異失?。簒xx redeclared in this block。
編譯重命名
初始化函數(shù)第三個特殊之處是:編譯重命名規(guī)則與普通函數(shù)不同。
普通函數(shù)在編譯過程中一般重命名規(guī)則為“[模塊名].包名.函數(shù)名”。
初始化函數(shù)在源碼中雖然名稱為init,但在編譯過程中重命名規(guī)則為“[模塊名].包名.init.數(shù)字后綴”。
例如:
- 在上述的 func_init.0.go 源文件編譯之后,init函數(shù)被重命名為:main.init.0。
- 在上述的 func_init.1.go 源文件編譯之后,兩個init函數(shù)分別被重命名為:main.init.0、main.init.1。
如上所示,如果同一個包下有多個init函數(shù),重命名時后綴數(shù)字按順序增加一。
為什么會這樣呢?
那是因為Golang編譯器對 init 函數(shù)進(jìn)行了特殊處理,相關(guān)源碼位于 cmd/compile/internal/gc/init.go 文件中。
全局變量 renameinitgen 用于記錄當(dāng)前包名下init函數(shù)的數(shù)量以及下一個init函數(shù)后綴的值。
每當(dāng)Golang編譯器遇到一個名稱為 init 的函數(shù),就會調(diào)用一次 renameinit() 函數(shù),最終 init 函數(shù)變得不可被調(diào)用。
為什么重命名init函數(shù)?
如上述我們看到的,在同一個包下可以重復(fù)聲明 init 函數(shù),這可能是需要重命名的原因。
當(dāng)我們繼續(xù)探究時,可能更加接近真相。
有一點需要明確并始終堅信:除全局常量和全局變量的聲明之外,所有的可執(zhí)行代碼都必須在函數(shù)內(nèi)執(zhí)行。
通常情況下,代碼編譯之后,
- 聲明的全局常量可能被存儲在可執(zhí)行文件的.rodata section。
- 聲明的全局變量可能被存儲在可執(zhí)行文件的.data、.bss、.noptrdata等section。
- 聲明的函數(shù)或方法被編譯為機(jī)器指令存儲在可執(zhí)行文件的.text section。
那么,以下代碼中(func_init.go),聲明全局變量的同時進(jìn)行初始化賦值,該如何編譯呢?
以下代碼屬于變量聲明。
- var m
- var name
而以下代碼包含函數(shù)調(diào)用和初始化賦值,最終要被編譯為機(jī)器指令,并且需要在main函數(shù)之前執(zhí)行;這些指令最終必須占用一塊存儲空間并且能夠加載到內(nèi)存中。
- var m = map[string]int{
- "Jack": 18,
- "Rose": 16,
- }
- var name = flag.String("name", "", "user name")
它們被存儲在可執(zhí)行文件的什么地方了呢?
通過逆向分析,發(fā)現(xiàn)Go編譯器合并了函數(shù)外的代碼調(diào)用(全局變量的初始化賦值),自動生成了一個 init 函數(shù);很明顯,在func_init.go源文件中并沒有定義初始化函數(shù)。
這可能也是編譯器重命名自定義init函數(shù)的原因吧。
編譯存儲
所有的初始化函數(shù)都不可被直接調(diào)用!所有它們會被存儲起來并在程序啟動時自動執(zhí)行。
在代碼編譯過程中,當(dāng)前包的初始化函數(shù)及其依賴的包的初始化,會被存儲到一個特殊的結(jié)構(gòu)體中,該結(jié)構(gòu)體定義在runtime/proc.go源文件中,如下所示:
- type initTask struct {
- state uintptr // 當(dāng)前包在程序運行時的初始化狀態(tài):0 = uninitialized, 1 = in progress, 2 = done
- ndeps uintptr // 當(dāng)前包的依賴包的數(shù)量
- nfns uintptr // 當(dāng)前包的初始化函數(shù)數(shù)量
- }
Go語言是一個語法糖很重的編程語言,在源碼中看到的往往不是真實的。
runtime.initTask結(jié)構(gòu)體是一個編譯時可修改的動態(tài)結(jié)構(gòu)。其真實面貌如下所示:
- type initTask struct {
- state uintptr // 當(dāng)前包在程序運行時的初始化狀態(tài):0 = uninitialized, 1 = in progress, 2 = done
- ndeps uintptr // 當(dāng)前包的依賴包的數(shù)量
- nfns uintptr // 當(dāng)前包的初始化函數(shù)數(shù)量
- deps [ndeps]*initTask // 當(dāng)前包的依賴包的initTask指針數(shù)組(不是slice)
- fns [nfns]func () // 當(dāng)前包的初始化函數(shù)指針數(shù)組(不是slice)
- }
每個包的依賴包數(shù)量可能不同(ndeps),每個包的初始化函數(shù)數(shù)量不同(nfns),所以最終生成的initTask對象大小可能不同。
具體編譯過程參考cmd/compile/internal/gc/init.go源文件中的fninit函數(shù),此處不再贅述。
Go編譯器為每個包生成一個runtime.initTask類型的全局變量,該變量的命名規(guī)則為“包名..inittask”,如下所示:
從上圖第三列可以看出,每個包的initTask對象大小不同。具體計算方法如下:
- size := (3 + ndeps + nfns) * 8
初始化過程
在可執(zhí)行程序啟動的初始化過程中,優(yōu)先執(zhí)行runtime包及其依賴包的初始化,然后執(zhí)行main包及其依賴包的初始化。
一個包可能被多個包依賴,但是每個包的都只初始化一次,通過runtime.initTask.state字段進(jìn)行控制。
具體的初始化邏輯請參考runtime/proc.go源文件中的main函數(shù)和doInit函數(shù)。
在初始化過程中,runtime.doInit函數(shù)會被調(diào)用很多次,其具體執(zhí)行流程如本文開頭的“包初始化”一節(jié)所述一致。
如前圖所示的func_init.2.go源文件,編譯之后包含兩個初始化函數(shù):一個是編譯器自動生成的,另一個是編譯器重命名的;自動生成的初始化函數(shù)優(yōu)先執(zhí)行。
如前圖所示的func_init.2.go源文件,編譯之后生成的main..inittask全局變量的內(nèi)存地址是0x000000000054dc60。我們動態(tài)調(diào)試runtime.doInit函數(shù),在其參數(shù)為main..inittask全局變量指針時暫停執(zhí)行,觀察參數(shù)的數(shù)據(jù)結(jié)構(gòu)。
從動態(tài)調(diào)試時展示的內(nèi)存數(shù)據(jù)我們反推出如下偽代碼:
- package main
- var inittask = struct {
- state uintptr // 當(dāng)前包在程序運行時的初始化狀態(tài):0 = uninitialized, 1 = in progress, 2 = done
- ndeps uintptr // 當(dāng)前包依賴的包的initTask數(shù)量
- nfns uintptr // 當(dāng)前包的初始化函數(shù)數(shù)量
- deps [2]uintptr // 當(dāng)前包依賴的包的initTask指針數(shù)組(不是slice)
- fns [2]uintptr // 當(dāng)前包的初始化函數(shù)指針數(shù)組(不是slice)
- }{
- state: 0,
- ndeps: 2,
- nfns: 2,
- deps: [2]uintptr{0x54ef60, 0x54eca0}, // flag..inittask,fmt..inittask
- fns: [2]uintptr{0x4a4ec0, 0x4a4d60}, // main.init,main.init.0
- }
在func_init.2.go源文件中,引用了flag、fmt兩個包,所以main包的初始化必須在這兩個包的初始化完成之后執(zhí)行。
- import "flag"
- import "fmt"
通常initTask.ndeps字段的值與import的數(shù)量相同。
編譯器自動生成的init函數(shù)先于代碼源文件中自定義的init函數(shù)執(zhí)行。
結(jié)語
至此,本文完整地、詳細(xì)地介紹了Go中關(guān)于初始化函數(shù)相關(guān)的內(nèi)容。
相信在認(rèn)真刨析了初始化函數(shù)的所有細(xì)節(jié)之后,對Go有了更近一步的了解。
希望有助于減少開發(fā)編碼過程中的疑惑,更加得心應(yīng)手,游刃有余。
本文轉(zhuǎn)載自微信公眾號「Golang In Memory」