一篇文章讀懂Golang?init函數執(zhí)行順序
1.init 函數簡介
Golang init 函數是一種特殊的函數,主要用于完成程序的初始化工作,如初始化數據庫的連接、載入本地配置文件、根據命令行參數初始化全局變量等。
package main import "flag" var gopath string func init() { println("init a") } func init() { println("init b") } func init() { println("init c") // gopath may be overridden by --gopath flag on command line. flag.StringVar(&gopath, "gopath", "/root/go", "override default GOPATH") } func main() { println("main") flag.Parse() println(gopath) }
運行輸出:
$ go run main.go --gopath="/home/alice/go"
init a
init b
init c
main
/home/alice/go
之所以特殊,是因為 init 函數有如下特點:
- init 函數是可選的,可以沒有;
- 與 main 函數一樣,不能有入參與返回值;
- 與 main 函數一樣,init 會自動執(zhí)行,不能被其他函數調用;
- 一個包內可以有多個 init 函數,即可以在包的多個源文件中定義多個 init 函數。一般建議在與包同名源文件中寫一個 init 函數,這樣可讀性好且便于維護;
- 一個源文件可以有多個 init 函數。
2.執(zhí)行順序
既然一個程序可以有多個 init 函數,那么對于位于不同包、不同源文件中的多個 init 函數,其執(zhí)行順序是怎樣的呢?
下面從多個方面去考察。
2.1 單個源文件的 init 執(zhí)行順序
package main func init() { println("init a") } func init() { println("init b") } func init() { println("init c") } func main() { println("main") }
運行輸出:
$ go run main.go
init a
init b
init c
main
結論: 同一個源文件的 init 函數執(zhí)行順序與其定義順序一致,從上到下。
2.2 單個包的 init 執(zhí)行順序
假設 main 包有三個源文件 a.go,b.go 和 c.go。
// a.go package main func init() { println("init a") } // b.go package main func init() { println("init b") } // c.go package main func init() { println("init c") } // main.go package main func init() { println("init main") } func main() { println("main") }
編譯運行輸出:
$ go build && ./main
init a
init b
init c
init main
main
結論: 同一個包中不同源文件 init 函數的執(zhí)行順序,是根據文件名的字典序來確定。
2.3 main 包導入多個包時 init 執(zhí)行順序
2.3.1 不存在依賴
對于不同的包,如果不相互依賴的話,在 main 包中被 import,那么這種情況下,各個包的 init 執(zhí)行順序是怎樣的呢?
假設有包 a,b 和 c,在 main.go 中被 import。
// a 包 // a.go package a func init() { println("init a") } // b 包 // b.go package b func init() { println("init b") } // c 包 // c.go package c func init() { println("init c") } // main 包 // main.go package main import ( _ "main/a" _ "main/b" _ "main/c" ) func init() { println("init main") } func main() { println("main") }
編譯運行輸出:
$ go build && ./main
init a
init b
init c
init main
main
結論: 對于不同的包,如果不相互依賴的話,按照 main 包中導入順序調用包的 init 函數,最后再調用 main 包的 init 函數。
2.3.2 存在依賴
對于不同的包,如果存在依賴關系的話,在 main 包中被 import,那么這種情況下,各個包的 init 執(zhí)行順序是怎樣的呢?
假設有包 a,b 和 c,main 包 import a 包,a import b,b import c,即依賴關系為 main > a > b > c。
// a 包 // a.go package a import _ "main/b" func init() { println("init a") } // b 包 // b.go package b import _ "main/c" func init() { println("init b") } // c 包 // c.go package c func init() { println("init c") } // main 包 // main.go package main import ( _ "main/a" ) func init() { println("init main") } func main() { println("main") }
編譯運行輸出:
$ go build && ./main
init c
init b
init a
init main
main
結論: 如果 package 存在依賴,不同包的 init 函數按照包導入的依賴關系決定執(zhí)行順序。 調用順序為最后被依賴的最先被初始化,如導入順序 main > a > b > c,則初始化順序為 c > b > a > main,依次執(zhí)行對應的 init 方法。
2.4 包級變量初始化與 init 函數執(zhí)行順序
如果包中存在包級變量,那么其初始化與 init 函數執(zhí)行先后順序如何呢?
還是假設有包 a,b 和 c,main 包 import a 包,a import b,b import c,即依賴關系為 main > a > b > c。且每個包都有一個包級變量,并通過函數完成其初始化。
// a 包 // a.go package a import _ "main/b" var A = func() string { println("init var A") return "A" }() func init() { println("init a") } // b 包 // b.go package b import _ "main/c" var B = func() string { println("init var B") return "B" }() func init() { println("init b") } // c 包 // c.go package c var C = func() string { println("init var C") return "C" }() func init() { println("init c") } // main 包 // main.go package main import ( _ "main/a" ) var m = func() string { println("init var m") return "m" }() func init() { println("init main") } func main() { println("main") }
編譯運行輸出:
$ go build && ./main
init var C
init c
init var B
init b
init var A
init a
init var m
init main
main
結論: 可見每個包的包級變量初始化是在 init 函數執(zhí)行之前完成的。不同包的 init 函數與包級變量的初始化順序如下圖所示。
3.小結
Golang 中的 init 是一種特殊的函數,主要用于完成程序的初始化工作。其特點有:
- init 函數是可選的,可以沒有;
- 與 main 函數一樣,不能有入參與返回值;
- 與 main 函數一樣,init 會自動執(zhí)行,不能被其他函數調用;
- 一個包內可以有多個 init 函數,即可以在包的多個源文件中定義多個 init 函數。一般建議在與包同名源文件中寫一個 init 函數,這樣可讀性好且便于維護;
- 一個源文件可以有多個 init 函數。
程序中如果在不同包的不同源文件有多個 init 函數時,其執(zhí)行順序可概述為:
- 同一個源文件的 init 函數執(zhí)行順序與其定義順序一致,從上到下;
- 同一個包中不同文件的 init 函數的執(zhí)行順序按照文件名的字典序;
- 對于不同的包,如果不相互依賴的話,按照 main 包中 import 的順序調用其包中的 init 函數;
- 如果包存在依賴,不同包的 init 函數按照包導入的依賴關系決定執(zhí)行順序。 調用順序為最后被依賴的最先被初始化,如導入順序 main > a > b > c,則初始化順序為 c > b > a > main,依次執(zhí)行對應的 init 方法;
- 如果包存在包級變量,則先于包的 init 函數完成初始化。
程序的初始化和執(zhí)行都起始于 main 包。如果 main 包還導入了其它的包,那么就會在編譯時將它們依次導入。有時一個包會被多個包同時導入,那么它只會被導入一次(例如很多包可能都會用到 fmt 包,但它只會被導入一次,因為沒有必要導入多次)。
當一個包被導入時,如果該包還導入了其它的包,那么會先將其它包導入進來,然后再對這些包中的包級常量和變量進行初始化,接著執(zhí)行 init 函數,依次類推。
請務必銘記于心,雖然 init() 順序是明確的,但代碼可以更改,init() 函數之間的關系可能會使代碼變得脆弱和容易出錯,因此在編碼時避免依賴 init() 函數的執(zhí)行順序。
參考文獻
- Package initialization - The Go Programming Language Specification
- Initialization - Effective Go
- 徹底搞懂下golang的init函數 - 嗶哩嗶哩
總結
到此這篇關于一篇文章讀懂Golang init函數執(zhí)行順序的文章就介紹到這了,更多相關Golang init函數執(zhí)行順序內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!