嗨,大家好!我是波羅學。本文是系列文章 Go 技巧第十六篇,系列文章查看:Go 語言技巧。
Go 中有一個特別的 init()
函數,它主要用于包的初始化。init()
函數在包被引入后會被自動執行。如果在 main
包中,它也會在 main()
函數之前執行。
本文將以此為主題,介紹 Go 中 init()
函數的使用和常見使用場景。還有,我在工作中更多看到的是 init()
函數的濫用。
init()
函數的執行時機
首先,init()
的執行時機處于包級別變量聲明和 main() 函數執行之間。
這意味著在包中聲明的全局變量,如果附帶初始化表達式,這些表達式將在任何 init()
函數執行之前進行初始化。
我們通過一個示例演示,代碼如下:
var Age = GetAge()func GetAge() int {return 18
}func init() {fmt.Printf("You're %d years old.\n", Age)Age = 3
}func main() {fmt.Printf("You're %d years old.\n", Age)
}
輸出:
You're 18 years old
You're 3 years old
從輸出可知,GetAge()
函數作為 Age
的初始化函數,于 init()
函數前執行,賦值 Age
為 3
。而 init()
函數于其后執行,賦值 Age
為 3
。main()
函數則在最后執行,輸出最終的 Age
值。
這個順序是符合我們預期的。
與被引入包的 init()
函數
如果一個包導入了其他包,被導入包的初始化 init()
則會先于導入它的包的變量初始化和 init
函數前執行。
舉例來說明吧!
假設,我們有一個 main
包,它導入了 sub
包,并且同樣有一個 init()
函數:
// main.gopackage mainimport ("fmt"_ "demo/sub"
)var age = GetAge()func GetAge() int {fmt.Println("main initialize variables.")return 18
}func init() {fmt.Println("main package init")
}func main() {fmt.Println("main function")
}
而 sub
包中包含定義的 init()
函數
// sub/sub.gopackage subimport "fmt"var age = GetAge()func GetAge() int {fmt.Println("sub initialize variables.")return 18
}func init() {fmt.Println("sub package init")
}// 其他可能的函數和聲明
當你運行 main.go
時,輸出將會按照以下順序出現:
sub initialize variables.
sub package init
main initialize variables.
main package init
main function
這個示例清晰地展示了包的初始化順序:首先是被導入包(sub
)的 init()
函數,然后是導入它的包(main
)的 init()
函數,最后是 main
函數。
這也確保了依賴包在使用前已經被正確初始化。
特別說明:
init()
區別于其他函數,不需要我們顯式調用,它會自動被 Go runtime 調用。而且,每個包中的init()
只會被執行一次。
一個包其實可有多個 init()
,無論是在分部在包中的同一個文件中還是多個文件中。如果分布在多個文件中,執行順序通常是按照文件名的字典順序。
為說明這個問題,我們首先修改 sub.go
文件,內容如下:
// sub/sub.gopackage subimport "fmt"var age = GetAge()func GetAge() int {fmt.Println("sub initialize variables.")return 18
}func init() {fmt.Println("sub init 1")
}func init() {fmt.Println("sub init 2")
}
新增一個 sub1.go
文件,如下所示:
// sub/sub1.gopackage subimport "fmt"var age = GetAge1()func GetAge1() int {fmt.Println("sub1 initialize variables.")return 18
}func init() {fmt.Println("sub1 init")
}
輸出:
sub initialize variables.
sub init 1
sub init 2
sub1 initialize variables.
sub1 init
main initialize variables.
main package init
main function
結果符合預期。
init()
的使用場景
init()
函數通常用于進行一些必要的設置或初始化操作,例如初始化包級別的變量與命令行參數、配置加載、環境檢查、甚至注冊插件等。
項目開發中,組件依賴管理通常比較令人頭疼。但一些簡單的依賴關系,即使沒有如 wire
這樣依賴注入工具的加持,通過 init
也可管理。
命令行參數
對于開發一個簡單的命令行應用,init()
和標準庫 flag
包結合,可快速完成命令命令行參數的初始化。
package mainimport ("flag""fmt"
)var name string
var help boolfunc init() {flag.StringVar(&name, "name", "World", "a name to say hello to")flag.StringVar(&help, "name", "World", "display help information")flag.Parse()
}func main() {if help {fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])flag.PrintDefaults()os.Exit(1)} fmt.Printf("Hello, %s!\n", name)
}
以上示例中,init()
函數解析了命令行參數并初始化變量 name
和 help
變量。
配置加載
init
函數的領哇一個常見場景是配置加載。配置通常是程序啟動時要盡早執行的操作。
例如,你有一個 web 服務,要在啟動服務器前加載數據庫配置、API 密鑰或其他服務配置。
var config AppConfigfunc init() {configFile, err := os.Open("config.json")if err != nil {log.Fatal(err)}defer configFile.Close()jsonParser := json.NewDecoder(configFile)jsonParser.Decode(&config)
}
如果配置加載都出現問題,很大程度說明服務配置不正常,要立刻退出服務。我們可使用 log.Fatal(err)
(更優雅)或 panic(err)
退出服務。
環境檢查
init()
還可以用于檢查和驗證程序運行所需的環境。如,我們要確保必要的環境變量已設置,或者必要的外部服務可用。
如我們的必須依賴一個需要認證的外部服務,示例代碼:
func init() {if os.Getenv("XXX_API_KEY") == "" {log.Fatal("XXX_API_KEY environment variable not set")}apiKey := os.Getenv("XXX_API_KEY")// instantiating Component// ...
}
通過,如果要實例化的組件不需要賴加載,創建和配置驗證同時 init()
中完成即可。
注冊插件或服務
如果你的程序用的是插件架構,我們可以在程序啟動時注冊這些插件。init()
正可以用來自動注冊這些插件。
示例代碼:
func init() {plugin.Register("myPlugin", NewMyPlugin)
}
Go 的數據庫驅動管理可作為這種場景的典型案例。
Go 的 database 操作通常依賴 database/sql
包,它提供了一種通用接口與 SQL 或類 SQL 數據庫交互。而具體的驅動實現(如 MySQL、PostgreSQL、SQLite 等)通常是通過實現 database/sql
包定義接口來提供支持。
這種架構下,init()
被用于驅動的自動注冊。
例如,如下這個 MySQL 驅動的實現:
package mysqlimport ("database/sql"
)func init() {sql.Register("mysql", &MySQLDriver{})
}type MySQLDriver struct {// 驅動的實現
}
我們只要導入這個 database driver 包,它的 init()
就會被調用,將驅動注冊到 database/sql
包中。
我們使用的時候,通過 database/sql
接口即可使用該 MySQL
驅動,而不需關心它的實現細節。
import ("database/sql"_ "github.com/go-sql-driver/mysql" // 導入 MySQL 驅動
)func main() {db, err := sql.Open("mysql", "user:password@/dbname")// ...
}
通過這種方式,Go 的數據庫驅動代碼更加模塊化和靈活性。使用方只需關心與 database/sql
交互即可,而不必關心驅動的實現細節。
實際的場景案例,我覺得肯定不止這么多。對于任何需要提前初始化和驗證的場景,可適當考慮是否可通過使用 init()
來簡化代碼。
注意點
講了那么多 init()
的使用,但我在平時發現,更多的時候 init()
函數是在被濫用。
我這里不得不提一些注意點。
啟動耗時
首先,由于 init()
函數在程序啟動時自動執行,這就導致它會增加程序啟動時間,特別是一些組件初始化耗時較長。
非必要場景,懶加載依然是不錯的選擇。
什么是必要場景呢?簡單來說,如果這個操作失敗了,這個程序就沒有繼續啟動的必要了。
依賴關系
還有,過多或過于復雜的 init()
函數可能會導致程序難以理解維護,依賴關系混亂。
這點在單體項目中體現的特別明顯,所有人維護一個項目,所以依賴都加載到 init()
中。
如何解決呢?
如前面所有,一方面要僅在必要場景時使用 init()
函數初始化一些操作。
另外,有條件的話,建議盡量保持服務簡單,如果依賴過多,如出現要一個服務連接多個相同組件(數據庫、Redis),就是時候考慮優化系統設計了,可考慮將部分業務抽離為獨立服務。
總結
本文介紹了到 init()
函數在 Go 中的特殊之處和使用方式。它提供了一種不同于其他語言的機制來初始化包,但也需謹慎使用以避免不必要的復雜性。
最后,希望這篇文章能幫助你更好地理解和使用 Go 的 init()
函數。
感謝閱讀。
博客地址:Go 中的 init 如何用?它的常見應用場景有哪些呢?