go
的 init
函數給人的感受怪怪的,我想不明白聰明的 google
團隊爲什麼要設計出這麼一個「雞肋「的機制。實際編碼中,我主張儘可能不要使用init
函數。mysql
首先來看看 init
函數的做用吧。c++
init()
介紹init()
與包的初始化順序息息相關,因此先介紹一個go
中包的初始化順序吧。(下面的內容部分摘自《The go programinng language》)git
大致而言,順序以下:程序員
init
函數main
函數應該任何強類型語言都是這樣子吧。github
例如:sql
var a = b + c; var b = f(); // 須要調用 f() 函數 var c = 1 func f() int{return c + 1;}
a
依賴 b
和 c
;b
依賴 f()
,f()
依賴 c
。所以,他們的初始化順序理所固然是 c -> b -> a
。數據庫
Ps:其實在這裏可能引伸出一個沒用的小技巧。當你有一個函數須要在包被初始化的過程當中被調用時,你能夠把這個函數賦值給一個包級變量。這樣,當包被初始化時就會自動調用這個函數了,這個函數甚至可以在
init()
以前被調用!不過話說回來,它既然比init()
更早被調用,那它纔是真正的init()
纔對;此外你也能夠在init()
中調用該函數,這樣才更合理一些。express// 笨版 // 函數必須得有一個返回值才行 var _ = func() interface{} { fmt.Println("hello") return nil }() func init() { fmt.Println("world") } func main() { } // Output: // hello // world// 更合理的版本 func init() { fmt.Println("hello") fmt.Println("world") } func main() { } // Output: // hello // world
一個包內每每有多個 go
文件,這麼go
文件的初始化順序由它們被提交給編譯器的順序決定,順序和這些文件的名字有關。ide
init()
主角出場了。先來看看它的設計動機吧:函數
Each variable declared at package level starts life with the value of its initializer expression, if any, but for some variables, like tables of data,an initializer expression may not be the simplest way to set its initial value.In that case,the init function mechanism may be simpler. 《The go pragramming language P44》
這句話的意思是有的包級變量沒辦法用一條簡單的表達式來初始化,這個 時候,init
機制就派上用場了。
init()
不能被調用,也不能被 reference,它們會在程序啓動時自動執行。
一個包內,甚至 go
文件內能夠包含多個 init()
,同一個 go
文件中的 init()
調用順序由他們的聲明順序決定 。
func init() { fmt.Print("a") } func init() { fmt.Print("b") } func init() { fmt.Print("c") } // Output // abc
go
文件中 init()
的調用順序依舊是由它們的聲明順序決定,同一個包下面的全部go
文件在編譯時會被編譯器合併成一個「大的go
文件「(並非真正合並,僅僅是效果相似而已)。合併的順序由編譯器決定。
不要把程序是否可以正常工做寄託在init()
可以按照你期待的順序被調用上。
不過話說回來,正經人誰在一個包裏寫不少 init()
呀,並且還把這些 init()
放在不一樣文件裏,更可惡的是每一個文件裏還有多個 init()
。要是看到這樣的代碼,我立馬:@#$%^&*...balabala...
一個包裏最多寫一個init()(我甚至以爲最好連一個 init()
都不要有)
惟獨這個順序,咱們程序員是絕對可控的。它們的調用順序由包之間的依賴關係決定。假設 a
包須要 import
b
包,b
包須要import
c
包,那麼很顯然他們的調用順序是,c
包的init()
最早被調用,其次是b
包,最後是a
包。
道理相似於一個變量最多會被初始化一次。
有的同窗會問,一個變量明明能夠屢次賦值呀,可第二次對這個變量賦值那還可以叫初始化麼?
例若有以下的包結構,B
包和C
包都分別import
A
包,D包須要import
B
包和C
包。
在 A
包中有 init()
func init() { fmt.Println("hello world") }
D
包是 main
包,最終程序只輸出了一句 hello world
。
我不喜歡 init
函數的一個重要緣由是,它會隱藏掉程序的一些細節,它會在沒有通過你贊成的狀況下,偷偷幹一些事情。go
的函數王國裏,全部的函數都須要程序員顯示的調用(Call)纔會被執行,只有它——init()
,是個例如,你明明沒 Call 它,它卻偷偷執行了。
有的同窗會說,c++
裏類的構造函數也是在對象被建立時就會默默執行呀。確實是這樣,但在 c++
裏,當你點進這個類的定義時,你就能立馬看到它的構造函數和析構函數。在 go
裏,當你點進某個包時,你能立馬看到包內的init()
麼?這個包有沒有init()
以及有幾個init()
徹底是個未知數,你須要在包內的全部文件中搜索 init()
這個關鍵字才能摸清包的 init()
狀況,而大多數人包括我懶得費這個功夫。在c++
中建立對象時,程序員可以很清楚的意識到這個操做會觸發這個類的構造函數,這個構造函數的內容也能很快找到;但在 go
中,import
包時,一切卻沒那麼清晰了。
但願未來 goland 或者 vscode 可以分析包內的 init()
狀況,這樣我對 init()
的惡意會減半。
init()
給項目維護帶來的困難當你看到這樣的 import
代碼時
import( _ "pkg" )
你立馬可以知道,這個 import
的目的就是調用 pkg
包的 int()
。
當看到
import( "pkg" )
你卻很難知道,pkg
包裏藏着一個 init()
,它被偷偷調用了。
但這還好,你起碼知道若是 pkg
包有 init()
的話,它會在此處被調用。
但當pkg
包,被多個包 import
時,pkg
包內的 init()
什麼時候被調用的,就是一個謎了。你得搞清楚這些包之間的 import
前後順序關係,這是一場噩夢。
init()
的時機先說一下個人結論:我認爲 init()
應該僅被用來初始化包內變量。
《The go programming language》提供了一個使用 init
函數的例子。
// pc[i] 是 i 中 bit = 1 的數量 var pc [256]byte func init() { for i := range pc { pc[i] = pc[i/2] + byte(i&1) } } // 返回 x 中等於 1 的 bit 的數量 func PopCount(x uint64) int { return int(pc[byte(x>>(0*8))] + pc[byte(x>>(1*8))] + pc[byte(x>>(2*8))] + pc[byte(x>>(3*8))] + pc[byte(x>>(4*8))] + pc[byte(x>>(5*8))] + pc[byte(x>>(6*8))] + pc[byte(x>>(7*8))]) }
PopCount
函數的做用數計算數字中等於 1
的 bit
的數量。例如 :
var i uint64 = 2
變量 i 的二進制表示形式爲
0000000000000000000000000000000000000000000000000000000000000010
把它傳入 PopCount
最終獲得的結果將爲 1
,由於它只有一個 bit
的值爲 1
。
pc
是一個表,它的 index 爲 x,其中 0 <= x <= 255,value 爲 x 中等於 1 的 bit 的數量。
它的初始化思想是:
若是一個數x最後的 bit 爲 1,那麼這個數值爲 1 的bit數 = x/2 的值爲1的bit數 + 1;
若是一個數x最後的 bit 爲 0,那麼這個數值爲 1 的bit數 = x/2 的值爲1的bit數;
在 PopCount
中把一個 8byte 數拆成了 8 個單 byte 數,分別計算這8個單 byte 數中 bit
爲 1
的數量,最後累加便可。
這裏 pc
的初始化確實比較複雜,沒法直接用
var pc = []byte{0, 1, 1,...}
這種形式給出。
一個能夠替代 init()
的方法是:
var pc = generatePc() func generatePc() [256]byte { var localPc [256]byte for i := range localPc { localPc[i] = localPc[i/2] + byte(i&1) } return localPc }
我以爲這樣子初始化比利用 init()
初始化要更好,由於你能夠立馬知道 pc
是怎樣得來的,而利用 init()
時,你須要利用 ide 來查找 pc
的 write reference,以後才能知道,哦,原來它(pc
)來這裏(init()
) 被初始化了呀。
當包內有多個變量的初始化流程比較複雜時,可能會寫出以下代碼。
var pc = generatePc() var pc2 = generatePc2() var pc3 = generatePc3() // ...
有的同窗可能不太喜歡這種寫法,那麼用上 init()
後,會寫成這樣
func init() { initPc() initPc2() initPc3() }
我以爲兩種寫法都說的過去吧,雖然我我的更傾向第一種寫法。
使用 init()
的時機,僅僅有一個例外,後面說。
init
函數的時機init()
除了初始化變量,不該該幹其餘任何事!
有兩個原則:
init()
不該該依賴包外的環境init()
不該該對包外的環境形成影響設置這兩個原則的理由是:任何對外部有依賴或者對外部有影響的代碼都有義務顯式的讓程序員知曉,不該該本身悄咪咪地去作,最好是顯式地讓程序員本身去調用。
init()
的活動範圍就應該僅僅被侷限在包內,本身和本身玩,不要影響了其餘小朋友的遊戲體驗。
以下幾條行爲就踩了紅線:
http
包中的 routeMap,會對 http
包形成影響,違反了第二條)反面教材就是:https://github.com/go-sql-driver/mysql 這個大名鼎鼎的包
當使用這個包時,一個必不可少的語句是:
import ( _ "github.com/go-sql-driver/mysql" )
緣由是它裏面有個 init
函數,會把本身註冊到 sql 包裏。
func init() { sql.Register("mysql", &MySQLDriver{}) }
按照以前的標準,此處明顯不符合規範,由於它影響了標準庫的 sql
包。
我認爲一個更好的方法是,建立一個 export
的專門用來作初始化工做的方法:
// Package mysql func Init() { sql.Register("mysql", &MySQLDriver{}) }
而後在 main
包中顯式的調用它:
// Package main func main(){ mysql.Init(); // other logic }
來比較一下兩種方式吧。
使用 Init()
是否須要告訴開發者額外的信息?
須要。
須要告訴開發者:使用這個庫時,記得必定要調用 Init() 哦,我在裏面作了一些工做。
開發者,點進 Init()
,瞬間瞭然。
是否可以阻止開發者不正確的調用?
不能。
由於是 export
的,因此開發者能夠想到哪兒調用就到哪兒調用,想調用多少次就調用多少次。
所以須要額外告訴開發者:請您務必只調用一次,以後就不要調用了。且必須在用到 sql
包以前調用,通常而言都是在 main()
的第一句調用。
使用 init()
是否須要告訴開發者額外的信息?
須要
依舊須要告訴開發者,必定要用 _ "github.com/go-sql-driver/mysql"
這個語句顯式的導入包哦,由於我利用init()
在裏面作一些工做。
開發者:那你作了什麼工做
庫:親,請您點進 mysql
包,在目錄下搜索 init()
關鍵字,慢慢找哦。
開發者:......
是否可以阻止開發者不正確的調用?
勉強能夠吧。
由於 init()
只會被調用一次,不可能被調用屢次,這從根本上杜絕了開發者調用屢次的可能性。
可你管不了開發者的 import
時機,假設開發者在其餘地方 import
了,致使你在 sql.Open()
時,mysql
的 driver
沒有被正常註冊,你仍是拿開發者沒有辦法。只能哀嘆一聲:我累了,毀滅吧。
我以爲做爲庫的提供者,最主要的是提供完善的機制,在用戶使用你的庫時,能利用你提供的機制,寫出無bug 的代碼。而不是像保姆同樣,千方百計避免用戶出錯。
因此可能使用 init()
爲了的優點就是減小了代碼量吧。
使用 Init()
時,須要兩句代碼
import ( "github.com/go-sql-driver/mysql" // 這句 ) func main(){ mysql.Init() // 這句 }
可是使用 init
時,卻只須要一句代碼
import ( _ "github.com/go-sql-driver/mysql" // 這句 )
oh yeah,足足少寫了一句代碼!
可能使用 init
的惟一例外就是寫單元測試的時候了吧。
假設我如今須要須要對 dao
層的增刪改查邏輯的寫一個單元測試。
func TestCURDPlayer(t *testing.T) { // 測試 curd 玩家信息 } func TestCURDStore(t *testing.T) { // 測試 curd 商店信息 } func TestCURDMail(t *testing.T) { // 測試 curd 郵件信息 }
很顯然,這些測試都是依賴數據庫的,所以爲了正常的測試,必須初始化數據庫
func TestCURDPlayer(t *testing.T) { // 測試 curd 玩家信息 initdb() // balabala } func TestCURDStore(t *testing.T) { // 測試 curd 商店信息 initdb() // balabala } func TestCURDMail(t *testing.T) { // 測試 curd 郵件信息 initdb() // balabala } func initdb(){ // sql.Open()... }
難道我每次新增一個單元測試,都要在單元測試的代碼中加一個 initdb()
麼,這也太麻煩了吧。
這個時候 init()
就派上用場了。能夠這樣
func TestCURDPlayer(t *testing.T) { // 測試 curd 玩家信息 // balabala } func TestCURDStore(t *testing.T) { // 測試 curd 商店信息 // balabala } func TestCURDMail(t *testing.T) { // 測試 curd 郵件信息 // balabala } func init(){ initdb() } func initdb(){ // sql.Open()... }
這樣,當對這個文件進行單元測試時,能夠確保在執行每一個 TestXXX
函數時,db
確定是被正確初始化了的。
那爲何這個地方能夠利用 init()
來初始化數據庫呢?
理由之一是它的影響範圍很小,僅僅在 xxx_test.go
文件中生效,在 go run
時不會起做用,在 go test
時纔會起做用。
理由之二是我懶。。。
init
更像是一個語法糖,它會讓開發者對代碼的追蹤能力變弱,因此能不用就最好不用。