詳解Go語(yǔ)言中init的使用與常見(jiàn)應(yīng)用場(chǎng)景
Go 中有一個(gè)特別的 init() 函數(shù),它主要用于包的初始化。init() 函數(shù)在包被引入后會(huì)被自動(dòng)執(zhí)行。如果在 main 包中,它也會(huì)在 main() 函數(shù)之前執(zhí)行。
本文將以此為主題,介紹 Go 中 init() 函數(shù)的使用和常見(jiàn)使用場(chǎng)景。還有,我在工作中更多看到的是 init() 函數(shù)的濫用。
init() 函數(shù)的執(zhí)行時(shí)機(jī)
首先,init() 的執(zhí)行時(shí)機(jī)處于包級(jí)別變量聲明和 main() 函數(shù)執(zhí)行之間。

這意味著在包中聲明的全局變量,如果附帶初始化表達(dá)式,這些表達(dá)式將在任何 init() 函數(shù)執(zhí)行之前進(jìn)行初始化。
我們通過(guò)一個(gè)示例演示,代碼如下:
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() 函數(shù)作為 Age 的初始化函數(shù),于 init() 函數(shù)前執(zhí)行,賦值 Age 為 3。而 init() 函數(shù)于其后執(zhí)行,賦值 Age 為 3。main() 函數(shù)則在最后執(zhí)行,輸出最終的 Age 值。
這個(gè)順序是符合我們預(yù)期的。
與被引入包的 init() 函數(shù)
如果一個(gè)包導(dǎo)入了其他包,被導(dǎo)入包的初始化 init() 則會(huì)先于導(dǎo)入它的包的變量初始化和 init 函數(shù)前執(zhí)行。

舉例來(lái)說(shuō)明吧!
假設(shè),我們有一個(gè) main 包,它導(dǎo)入了 sub 包,并且同樣有一個(gè) init() 函數(shù):
// main.go
package main
import (
"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() 函數(shù)
// sub/sub.go
package sub
import "fmt"
var age = GetAge()
func GetAge() int {
fmt.Println("sub initialize variables.")
return 18
}
func init() {
fmt.Println("sub package init")
}
// 其他可能的函數(shù)和聲明當(dāng)你運(yùn)行 main.go 時(shí),輸出將會(huì)按照以下順序出現(xiàn):
sub initialize variables.
sub package init
main initialize variables.
main package init
main function
這個(gè)示例清晰地展示了包的初始化順序:首先是被導(dǎo)入包(sub)的 init() 函數(shù),然后是導(dǎo)入它的包(main)的 init() 函數(shù),最后是 main 函數(shù)。
這也確保了依賴包在使用前已經(jīng)被正確初始化。
特別說(shuō)明:
init() 區(qū)別于其他函數(shù),不需要我們顯式調(diào)用,它會(huì)自動(dòng)被 Go runtime 調(diào)用。而且,每個(gè)包中的 init() 只會(huì)被執(zhí)行一次。
一個(gè)包其實(shí)可有多個(gè) init(),無(wú)論是在分部在包中的同一個(gè)文件中還是多個(gè)文件中。如果分布在多個(gè)文件中,執(zhí)行順序通常是按照文件名的字典順序。

為說(shuō)明這個(gè)問(wèn)題,我們首先修改 sub.go 文件,內(nèi)容如下:
// sub/sub.go
package sub
import "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")
}新增一個(gè) sub1.go 文件,如下所示:
// sub/sub1.go
package sub
import "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
結(jié)果符合預(yù)期。
init() 的使用場(chǎng)景
init() 函數(shù)通常用于進(jìn)行一些必要的設(shè)置或初始化操作,例如初始化包級(jí)別的變量與命令行參數(shù)、配置加載、環(huán)境檢查、甚至注冊(cè)插件等。

項(xiàng)目開(kāi)發(fā)中,組件依賴管理通常比較令人頭疼。但一些簡(jiǎn)單的依賴關(guān)系,即使沒(méi)有如 wire 這樣依賴注入工具的加持,通過(guò) init 也可管理。
命令行參數(shù)
對(duì)于開(kāi)發(fā)一個(gè)簡(jiǎn)單的命令行應(yīng)用,init() 和標(biāo)準(zhǔn)庫(kù) flag 包結(jié)合,可快速完成命令命令行參數(shù)的初始化。
package main
import (
"flag"
"fmt"
)
var name string
var help bool
func 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() 函數(shù)解析了命令行參數(shù)并初始化變量 name 和 help 變量。
配置加載
init 函數(shù)的領(lǐng)哇一個(gè)常見(jiàn)場(chǎng)景是配置加載。配置通常是程序啟動(dòng)時(shí)要盡早執(zhí)行的操作。
例如,你有一個(gè) web 服務(wù),要在啟動(dòng)服務(wù)器前加載數(shù)據(jù)庫(kù)配置、API 密鑰或其他服務(wù)配置。
var config AppConfig
func init() {
configFile, err := os.Open("config.json")
if err != nil {
log.Fatal(err)
}
defer configFile.Close()
jsonParser := json.NewDecoder(configFile)
jsonParser.Decode(&config)
}如果配置加載都出現(xiàn)問(wèn)題,很大程度說(shuō)明服務(wù)配置不正常,要立刻退出服務(wù)。我們可使用 log.Fatal(err) (更優(yōu)雅)或 panic(err) 退出服務(wù)。
環(huán)境檢查
init() 還可以用于檢查和驗(yàn)證程序運(yùn)行所需的環(huán)境。如,我們要確保必要的環(huán)境變量已設(shè)置,或者必要的外部服務(wù)可用。
如我們的必須依賴一個(gè)需要認(rèn)證的外部服務(wù),示例代碼:
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
// ...
}通過(guò),如果要實(shí)例化的組件不需要賴加載,創(chuàng)建和配置驗(yàn)證同時(shí) init() 中完成即可。
注冊(cè)插件或服務(wù)
如果你的程序用的是插件架構(gòu),我們可以在程序啟動(dòng)時(shí)注冊(cè)這些插件。init() 正可以用來(lái)自動(dòng)注冊(cè)這些插件。
示例代碼:
func init() {
plugin.Register("myPlugin", NewMyPlugin)
}Go 的數(shù)據(jù)庫(kù)驅(qū)動(dòng)管理可作為這種場(chǎng)景的典型案例。
Go 的 database 操作通常依賴 database/sql 包,它提供了一種通用接口與 SQL 或類 SQL 數(shù)據(jù)庫(kù)交互。而具體的驅(qū)動(dòng)實(shí)現(xiàn)(如 MySQL、PostgreSQL、SQLite 等)通常是通過(guò)實(shí)現(xiàn) database/sql 包定義接口來(lái)提供支持。
這種架構(gòu)下,init() 被用于驅(qū)動(dòng)的自動(dòng)注冊(cè)。
例如,如下這個(gè) MySQL 驅(qū)動(dòng)的實(shí)現(xiàn):
package mysql
import (
"database/sql"
)
func init() {
sql.Register("mysql", &MySQLDriver{})
}
type MySQLDriver struct {
// 驅(qū)動(dòng)的實(shí)現(xiàn)
}我們只要導(dǎo)入這個(gè) database driver 包,它的 init() 就會(huì)被調(diào)用,將驅(qū)動(dòng)注冊(cè)到 database/sql 包中。
我們使用的時(shí)候,通過(guò) database/sql 接口即可使用該 MySQL 驅(qū)動(dòng),而不需關(guān)心它的實(shí)現(xiàn)細(xì)節(jié)。
import (
"database/sql"
_ "github.com/go-sql-driver/mysql" // 導(dǎo)入 MySQL 驅(qū)動(dòng)
)
func main() {
db, err := sql.Open("mysql", "user:password@/dbname")
// ...
}通過(guò)這種方式,Go 的數(shù)據(jù)庫(kù)驅(qū)動(dòng)代碼更加模塊化和靈活性。使用方只需關(guān)心與 database/sql 交互即可,而不必關(guān)心驅(qū)動(dòng)的實(shí)現(xiàn)細(xì)節(jié)。
實(shí)際的場(chǎng)景案例,我覺(jué)得肯定不止這么多。對(duì)于任何需要提前初始化和驗(yàn)證的場(chǎng)景,可適當(dāng)考慮是否可通過(guò)使用 init() 來(lái)簡(jiǎn)化代碼。
注意點(diǎn)
講了那么多 init() 的使用,但我在平時(shí)發(fā)現(xiàn),更多的時(shí)候 init() 函數(shù)是在被濫用。
我這里不得不提一些注意點(diǎn)。

啟動(dòng)耗時(shí)
首先,由于 init() 函數(shù)在程序啟動(dòng)時(shí)自動(dòng)執(zhí)行,這就導(dǎo)致它會(huì)增加程序啟動(dòng)時(shí)間,特別是一些組件初始化耗時(shí)較長(zhǎng)。
非必要場(chǎng)景,懶加載依然是不錯(cuò)的選擇。
什么是必要場(chǎng)景呢?簡(jiǎn)單來(lái)說(shuō),如果這個(gè)操作失敗了,這個(gè)程序就沒(méi)有繼續(xù)啟動(dòng)的必要了。
依賴關(guān)系
還有,過(guò)多或過(guò)于復(fù)雜的 init() 函數(shù)可能會(huì)導(dǎo)致程序難以理解維護(hù),依賴關(guān)系混亂。
這點(diǎn)在單體項(xiàng)目中體現(xiàn)的特別明顯,所有人維護(hù)一個(gè)項(xiàng)目,所以依賴都加載到 init() 中。
如何解決呢?
如前面所有,一方面要僅在必要場(chǎng)景時(shí)使用 init() 函數(shù)初始化一些操作。
另外,有條件的話,建議盡量保持服務(wù)簡(jiǎn)單,如果依賴過(guò)多,如出現(xiàn)要一個(gè)服務(wù)連接多個(gè)相同組件(數(shù)據(jù)庫(kù)、Redis),就是時(shí)候考慮優(yōu)化系統(tǒng)設(shè)計(jì)了,可考慮將部分業(yè)務(wù)抽離為獨(dú)立服務(wù)。
總結(jié)
本文介紹了到 init() 函數(shù)在 Go 中的特殊之處和使用方式。它提供了一種不同于其他語(yǔ)言的機(jī)制來(lái)初始化包,但也需謹(jǐn)慎使用以避免不必要的復(fù)雜性。
到此這篇關(guān)于詳解Go語(yǔ)言中init的使用與常見(jiàn)應(yīng)用場(chǎng)景的文章就介紹到這了,更多相關(guān)Go init使用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語(yǔ)言中for循環(huán)的經(jīng)典案例分析
for循環(huán)問(wèn)題,在面試中經(jīng)常都會(huì)被問(wèn)到,并且在實(shí)際業(yè)務(wù)項(xiàng)目中也經(jīng)常用到for循環(huán),要是沒(méi)用好,一不下心就掉坑。本文為大家挑選了幾個(gè)經(jīng)典的案例,一塊來(lái)探討下,看看如何避免掉坑,多積累積累采坑經(jīng)驗(yàn)2023-02-02
Go?modules?replace解決Go依賴引用問(wèn)題
這篇文章主要為大家介紹了Go?modules?replace解決Go依賴引用問(wèn)題,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
go的defer和閉包示例說(shuō)明(非內(nèi)部實(shí)現(xiàn))
這篇文章主要為大家介紹了go的defer和閉包示例說(shuō)明(非內(nèi)部實(shí)現(xiàn)),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
GoLang bytes.Buffer基礎(chǔ)使用方法詳解
Go標(biāo)準(zhǔn)庫(kù)中的bytes.Buffer(下文用Buffer表示)類似于一個(gè)FIFO的隊(duì)列,它是一個(gè)流式字節(jié)緩沖區(qū),我們可以持續(xù)向Buffer尾部寫(xiě)入數(shù)據(jù),從Buffer頭部讀取數(shù)據(jù)。當(dāng)Buffer內(nèi)部空間不足以滿足寫(xiě)入數(shù)據(jù)的大小時(shí),會(huì)自動(dòng)擴(kuò)容2023-03-03
golang強(qiáng)制類型轉(zhuǎn)換和類型斷言
這篇文章主要介紹了詳情介紹golang類型轉(zhuǎn)換問(wèn)題,分別由介紹類型斷言和類型轉(zhuǎn)換,這兩者都是不同的概念,下面文章圍繞類型斷言和類型轉(zhuǎn)換的相關(guān)資料展開(kāi)文章的詳細(xì)內(nèi)容,需要的朋友可以參考以下2021-12-12
Golang使用JWT進(jìn)行認(rèn)證和加密的示例詳解
JWT是一個(gè)簽名的JSON對(duì)象,通常用作Oauth2的Bearer?token,JWT包括三個(gè)用.分割的部分。本文將利用JWT進(jìn)行認(rèn)證和加密,感興趣的可以了解一下2023-02-02
Go語(yǔ)言Elasticsearch數(shù)據(jù)清理工具思路詳解
這篇文章主要介紹了Go語(yǔ)言Elasticsearch數(shù)據(jù)清理工具思路詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-10-10
golang?cache帶索引超時(shí)緩存庫(kù)實(shí)戰(zhàn)示例
這篇文章主要為大家介紹了golang?cache帶索引超時(shí)緩存庫(kù)實(shí)戰(zhàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09

