Go:神奇的 init 函數
前言
今天與大家聊一聊
Go
語言中的神奇函數init
,爲什麼叫他神奇函數呢?因爲該函數可以在所有程序執行開始前被調用,並且每個包下可以有多個init
函數。這個函數使用起來比較簡單,但是你們知道他的執行順序是怎樣的嘛?本文我們就一起來解密。
init
函數的特性
先簡單介紹一下init
函數的基本特性:
-
init
函數先於main
函數自動執行 -
每個包中可以有多個
init
函數,每個包中的源文件中也可以有多個init
函數 -
init
函數沒有輸入參數、返回值,也未聲明,所以無法引用 -
不同包的
init
函數按照包導入的依賴關係決定執行順序 -
無論包被導入多少次,
init
函數只會被調用一次,也就是隻執行一次
init
函數的執行順序
我在剛學習init
函數時就對他的執行順序很好奇,在谷歌上搜了幾篇文章,他們都有一樣的圖:
下圖來源於網絡:
截屏 2021-06-05 上午 9.55.15
這張圖片很清晰的反應了init
函數的加載順序:
-
包加載優先級排在第一位,先層層遞歸進行包加載
-
每個包中加載順序爲:
const
>var
>init
,首先進行初始化的是常量,然後是變量,最後纔是init
函數。針對包級別的變量初始化順序,Go
官方文檔給出這樣一個例子:
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
。
上圖只是表達了init
函數大概的加載順序,有些細節我們還是不知道的,比如:當前包下有多個init
函數,按照什麼順序執行,當前源文件下有多個init
函數,這又按照什麼順序執行呢?本來想寫個例子挨個驗證一下的,後來一看Go
官方文檔中都有說明,也就沒有必要再寫一個例子啦,直接說結論吧:
-
如果當前包下有多個
init
函數,首先按照源文件名的字典序從前往後執行。 -
若一個文件中出現多個
init
函數,則按照出現順序從前往後執行。
前面說的有點亂,對init
函數的加載順序做一個小結:
從當前包開始,如果當前包包含多個依賴包,則先初始化依賴包,層層遞歸初始化各個包,在每一個包中,按照源文件的字典序從前往後執行,每一個源文件中,優先初始化常量、變量,最後初始化
init
函數,當出現多個init
函數時,則按照順序從前往後依次執行,每一個包完成加載後,遞歸返回,最後在初始化當前包!
init
函數的使用場景
還記得我之前的這篇文章嗎:go 解鎖設計模式之單例模式,借用init
函數的加載機制我們可以實現單例模式中的餓漢模式,具體怎麼實現可以參考這篇文章,這裏就不在寫一遍了。
init
函數的使用場景還是挺多的,比如進行服務註冊、進行數據庫或各種中間件的初始化連接等。Go
的標準庫中也有許多地方使用到了init
函數,比如我們經常使用的pprof
工具,他就使用到了init
函數,在init
函數里面進行路由註冊:
//go/1.15.7/libexec/src/cmd/trace/pprof.go
func init() {
http.HandleFunc("/io", serveSVGProfile(pprofByGoroutine(computePprofIO)))
http.HandleFunc("/block", serveSVGProfile(pprofByGoroutine(computePprofBlock)))
http.HandleFunc("/syscall", serveSVGProfile(pprofByGoroutine(computePprofSyscall)))
http.HandleFunc("/sched", serveSVGProfile(pprofByGoroutine(computePprofSched)))
http.HandleFunc("/regionio", serveSVGProfile(pprofByRegion(computePprofIO)))
http.HandleFunc("/regionblock", serveSVGProfile(pprofByRegion(computePprofBlock)))
http.HandleFunc("/regionsyscall", serveSVGProfile(pprofByRegion(computePprofSyscall)))
http.HandleFunc("/regionsched", serveSVGProfile(pprofByRegion(computePprofSched)))
}
這裏就不擴展太多了,更多標準庫中的使用方法大家可以自己去探索一下。
在這最後總結一下使用init
要注意的問題吧:
-
編程時不要依賴
init
的順序 -
一個源文件下可以有多個
init
函數,代碼比較長時可以考慮分多個init
函數 -
複雜邏輯不建議使用
init
函數,會增加代碼的複雜性,可讀性也會下降 -
在
init
函數中也可以啓動goroutine
,也就是在初始化的同時啓動新的goroutine
,這並不會影響初始化順序 -
init
函數不應該依賴任何在main
函數里創建的變量,因爲init
函數的執行是在main
函數之前的 -
init
函數在代碼中不能被顯示的調用,不能被引用(賦值給函數變量),否則會出現編譯錯誤。 -
導入包不要出現循環依賴,這樣會導致程序編譯失敗
-
Go
程序僅僅想要用一個package
的init
執行,我們可以這樣使用:import _ "test_xxxx"
,導入包的時候加上下劃線就ok
了 -
包級別的變量初始化、
init
函數執行,這兩個操作都是在同一個goroutine
中調用的,按順序調用,一次一個包
總結
好啦,這篇文章到這裏就結束了,本身init
函數就很好理解,寫這篇文章的目的就是讓大家瞭解他的執行順序,這樣在日常開發中纔不會寫出bug
。希望本文對大家有所幫助,我們下期見!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/xag2pzC8LDqhcjcXeNxVrA