長(zhǎng)沙建個(gè)網(wǎng)站一般需要多少錢網(wǎng)絡(luò)營(yíng)銷策劃方案
Go包介紹與初始化:搞清Go程序的執(zhí)行次序
文章目錄
- Go包介紹與初始化:搞清Go程序的執(zhí)行次序
- 一、main.main 函數(shù):Go 應(yīng)用的入口函數(shù)
- 1.1 main.main 函數(shù)
- 1.2 main.main 函數(shù)特點(diǎn)
- 二、包介紹
- 2.1 包介紹與聲明
- 2.2 非 main包的 main 函數(shù)
- 2.3 包的命名規(guī)則
- 三、包的導(dǎo)入
- 3.1 包的導(dǎo)入介紹
- 3.2 導(dǎo)入多個(gè)包
- 3.2 包的別名
- 四、神器的下劃線
- 4.1 下劃線的作用
- 4.2 下劃線在代碼中
- 4.3 下劃線在import中
- 五、init 函數(shù):Go 包的初始化函數(shù)
- 5.1 init 函數(shù) 介紹
- 5.2 init 函數(shù) 特點(diǎn)
- 六、Go 包的初始化次序
- 6.1 包的初始化次序探究
- 6.2 包的初始化原則
- 七、init 函數(shù)的常用用途
- 7.1 用途一:重置包級(jí)變量值
- 7.2 用途二:實(shí)現(xiàn)對(duì)包級(jí)變量的復(fù)雜初始化
- 7.3 用途三:在 init 函數(shù)中實(shí)現(xiàn)“注冊(cè)模式”
一、main.main 函數(shù):Go 應(yīng)用的入口函數(shù)
1.1 main.main 函數(shù)
在Go語(yǔ)言中,main
函數(shù)是任何Go應(yīng)用的入口函數(shù)–用戶層入口。當(dāng)你運(yùn)行一個(gè)Go程序時(shí),操作系統(tǒng)會(huì)首先調(diào)用main
函數(shù),然后程序開(kāi)始執(zhí)行。main
函數(shù)的函數(shù)原型是這樣的:
package mainfunc main() {// 用戶層執(zhí)行邏輯... ...
}
你的程序的執(zhí)行會(huì)從main
函數(shù)開(kāi)始,會(huì)在這個(gè)函數(shù)內(nèi)按照它的調(diào)用順序展開(kāi)。
1.2 main.main 函數(shù)特點(diǎn)
main.main
函數(shù)是Go應(yīng)用程序的入口函數(shù),它具有一些特點(diǎn)和規(guī)定,使得Go程序的執(zhí)行流程有一定的規(guī)范性。以下是關(guān)于main.main
函數(shù)的特點(diǎn):
- 唯一入口點(diǎn): 在一個(gè)Go應(yīng)用程序中,只能有一個(gè)
main.main
函數(shù)。這是整個(gè)程序的唯一入口點(diǎn),程序的執(zhí)行將從這里開(kāi)始。如果存在多個(gè)main
函數(shù),編譯時(shí)會(huì)報(bào)錯(cuò)。 - 不接受參數(shù):
main.main
函數(shù)不接受任何參數(shù),它沒(méi)有輸入?yún)?shù),也沒(méi)有返回值。這是Go語(yǔ)言的規(guī)定,而程序的命令行參數(shù)通常通過(guò)os.Args
等方式獲取。
二、包介紹
2.1 包介紹與聲明
在Go中,包(Package)是組織和管理代碼的基本單元。包包括一組相關(guān)的函數(shù)、類型和變量,它們可以被導(dǎo)入到其他Go文件中以便重復(fù)使用。Go標(biāo)準(zhǔn)庫(kù)以及第三方庫(kù)都是以包的形式提供的。
每個(gè)Go文件都屬于一個(gè)包,你可以使用package
關(guān)鍵字來(lái)指定聲明一個(gè)文件屬于哪個(gè)包。例如:
package main
2.2 非 main包的 main 函數(shù)
除了 main 包外,其他包也可以擁有自己的名為 main 的函數(shù)或方法。但按照 Go 的可見(jiàn)性規(guī)則(小寫字母開(kāi)頭的標(biāo)識(shí)符為非導(dǎo)出標(biāo)識(shí)符),**非 main 包中自定義的 main 函數(shù)僅限于包內(nèi)使用,**就像下面代碼這樣,這是一段在非 main 包中定義 main 函數(shù)的代碼片段:
package pkg1import "fmt"func Main() {main()
}func main() {fmt.Println("main func for pkg1")
}
你可以看到,這里 main 函數(shù)就主要是用來(lái)在包 pkg1
內(nèi)部使用的,它是沒(méi)法在包外使用的。
2.3 包的命名規(guī)則
- 在Go語(yǔ)言中,包的名稱通常使用小寫字母,具有簡(jiǎn)潔的、描述性的名稱。這有助于提高代碼的可讀性和可維護(hù)性。標(biāo)準(zhǔn)庫(kù)中的包通常具有非常清晰的包名,例如
fmt
、math
、strings
等。 - 在Go語(yǔ)言中,包級(jí)別的標(biāo)識(shí)符(變量、函數(shù)、類型等)的可見(jiàn)性是由其首字母的大小寫來(lái)決定的。如果一個(gè)標(biāo)識(shí)符以大寫字母開(kāi)頭,它就是可導(dǎo)出的(公有的),可以被其他包訪問(wèn)。如果以小寫字母開(kāi)頭,它就是包內(nèi)私有的,只能在包內(nèi)部使用。
三、包的導(dǎo)入
3.1 包的導(dǎo)入介紹
要在Go程序中使用其他包的功能,你需要導(dǎo)入這些包。使用import
關(guān)鍵字來(lái)導(dǎo)入包,導(dǎo)入語(yǔ)句通常放在文件的頂部。一個(gè)典型的包導(dǎo)入語(yǔ)句的格式如下:
import "包的導(dǎo)入路徑"
其中,包的導(dǎo)入路徑
是指被導(dǎo)入包的唯一標(biāo)識(shí)符,通常是包的名稱或路徑,它用于告訴Go編譯器去哪里找到這個(gè)包的代碼。
例如,導(dǎo)入標(biāo)準(zhǔn)庫(kù)中的fmt
包可以這樣做:
import "fmt"
然后,你就可以在你的程序中使用fmt
包提供的函數(shù)和類型。
3.2 導(dǎo)入多個(gè)包
在Go程序中,你可以一次導(dǎo)入多個(gè)包,只需在import
語(yǔ)句中列出這些包的導(dǎo)入路徑,用括號(hào)()括起來(lái)并以括號(hào)內(nèi)的方式分隔包的導(dǎo)入路徑。
示例:
import ("fmt""math""net/http"
)
這個(gè)示例中導(dǎo)入了fmt
、math
和net/http
三個(gè)包。這種方式使你可以更清晰地組織你的導(dǎo)入語(yǔ)句,以便程序更易讀。
注意:Go語(yǔ)言的編譯器會(huì)自動(dòng)檢測(cè)哪些導(dǎo)入的包是真正被使用的,未使用的導(dǎo)入包不會(huì)引起編譯錯(cuò)誤,但通常被視為不良實(shí)踐。在Go中,未使用的導(dǎo)入包可能會(huì)引起代碼不清晰,因此應(yīng)該避免導(dǎo)入不需要的包。
3.2 包的別名
在Go語(yǔ)言中,你可以使用包的別名(package alias)來(lái)為一個(gè)導(dǎo)入的包賦予一個(gè)不同的名稱,以便在代碼中引用它。包的別名通常用于以下情況:
- 避免包名沖突:當(dāng)你導(dǎo)入多個(gè)包時(shí),有可能出現(xiàn)包名沖突,此時(shí)你可以為一個(gè)或多個(gè)包使用別名來(lái)解決沖突。
- 簡(jiǎn)化包名:有時(shí),包的導(dǎo)入路徑可能很長(zhǎng),為了減少代碼中的冗長(zhǎng),你可以為包使用一個(gè)短的別名。
使用包的別名是非常簡(jiǎn)單的,只需在導(dǎo)入語(yǔ)句中使用as
關(guān)鍵字為包指定一個(gè)別名。以下是示例:
import fm "fmt"
在上面的示例中,fm
是fmt
包的別名?,F(xiàn)在,你可以在代碼中使用fm
來(lái)代替fmt
,例如:
fm.Println("Hello, World!")
這樣,你就可以使用更短的fm
來(lái)調(diào)用fmt
包的函數(shù),以減少代碼中的冗長(zhǎng)。
包的別名可以根據(jù)需要自定義,但通常建議選擇一個(gè)有意義的別名,以使代碼更易讀。使用別名時(shí)要注意避免產(chǎn)生混淆,要確保別名不與其他標(biāo)識(shí)符(如變量名或函數(shù)名)發(fā)生沖突。
四、神器的下劃線
4.1 下劃線的作用
下劃線 _
在Go語(yǔ)言中用于以下幾個(gè)不同的場(chǎng)景:
- 匿名變量:
_
可以用作匿名變量,用于忽略某個(gè)值。當(dāng)你希望某個(gè)值返回但又不需要使用它時(shí),可以將其賦值給_
。 - 空標(biāo)識(shí)符:
_
也被稱為空標(biāo)識(shí)符,它用于聲明但不使用變量或?qū)氚皇褂冒臉?biāo)識(shí)符。這是為了確保代碼通過(guò)編譯,但不會(huì)產(chǎn)生未使用變量或包的警告。
4.2 下劃線在代碼中
在代碼中,下劃線 _
可以用作匿名變量,用于忽略某個(gè)值。這通常在函數(shù)多返回值中使用,如果你只關(guān)心其中的某些值而不需要其他返回值,可以將其賦值給 _
。
示例:
x, _ := someFunction() // 忽略第二個(gè)返回值
在上面的示例中,_
用于忽略 someFunction
函數(shù)的第二個(gè)返回值。
4.3 下劃線在import中
- 當(dāng)導(dǎo)入一個(gè)包時(shí),該包下的文件里所有
init()
函數(shù)都會(huì)被執(zhí)行,然而,有些時(shí)候我們并不需要把整個(gè)包都導(dǎo)入進(jìn)來(lái),僅僅是是希望它執(zhí)行init()
函數(shù)而已。 - 這個(gè)時(shí)候就可以使用
import _
引用該包。即使用 import _ 包路徑 只是引用該包,僅僅是為了調(diào)用init()函數(shù),所以無(wú)法通過(guò)包名來(lái)調(diào)用包中的其他函數(shù)。
以下是一個(gè)示例,演示如何使用 import _
引用一個(gè)包以執(zhí)行其 init()
函數(shù):
項(xiàng)目結(jié)構(gòu):
src
|
+--- main.go
|
+--- hello|+--- hello.go
main.go
文件
package mainimport _ "./hello"func main() {// hello.Print() //編譯報(bào)錯(cuò):./main.go:6:5: undefined: hello
}
hello.go
文件
package helloimport "fmt"func init() {fmt.Println("imp-init() come here.")
}func Print() {fmt.Println("Hello!")
}
輸出結(jié)果:
imp-init() come here.
五、init 函數(shù):Go 包的初始化函數(shù)
5.1 init 函數(shù) 介紹
init
函數(shù)是在Go包的初始化階段自動(dòng)調(diào)用的函數(shù)。它的目的是執(zhí)行一些包級(jí)別的初始化工作,例如設(shè)置變量、初始化數(shù)據(jù)、連接數(shù)據(jù)庫(kù)等。init
函數(shù)沒(méi)有參數(shù),也沒(méi)有返回值,它的定義形式如下:
func init() {// 包初始化邏輯... ...
}
5.2 init 函數(shù) 特點(diǎn)
init
函數(shù)有以下特點(diǎn):
- 自動(dòng)執(zhí)行:
init
函數(shù)不需要手動(dòng)調(diào)用,它會(huì)在程序啟動(dòng)時(shí)自動(dòng)執(zhí)行。這確保了包的初始化工作在程序開(kāi)始執(zhí)行之前完成。 - 包級(jí)別:
init
函數(shù)是包級(jí)別的,因此它只能在包的內(nèi)部定義。不同包中的init
函數(shù)互不影響,它們獨(dú)立執(zhí)行。 - 多個(gè)
init
函數(shù): 一個(gè)包可以包含多個(gè)init
函數(shù),它們按照定義的順序依次執(zhí)行。被導(dǎo)入的包的init
函數(shù)會(huì)在導(dǎo)入它的包的init
函數(shù)之前執(zhí)行。 - 沒(méi)有參數(shù)和返回值: 和前面
main.main
函數(shù)一樣,init
函數(shù)也是一個(gè)無(wú)參數(shù)無(wú)返回值的函數(shù),它只用于執(zhí)行初始化工作,而不與其他函數(shù)交互。 - 順序執(zhí)行: 由于
init
函數(shù)的執(zhí)行順序是根據(jù)包的導(dǎo)入順序確定的,因此在編寫代碼時(shí)應(yīng)該謹(jǐn)慎考慮包的依賴關(guān)系,以確保正確的初始化順序。 - 可用于注冊(cè)和初始化:
init
函數(shù)通常用于執(zhí)行包的初始化工作,也可用于在導(dǎo)入包時(shí)注冊(cè)一些功能,例如數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序的注冊(cè)。
這里要特別注意的是,在 Go 程序中我們不能手工顯式地調(diào)用 init
,否則就會(huì)收到編譯錯(cuò)誤,就像下面這個(gè)示例,它表示的手工顯式調(diào)用 init 函數(shù)的錯(cuò)誤做法:
package mainimport "fmt"func init() {fmt.Println("init invoked")
}func main() {init()
}
構(gòu)建并運(yùn)行上面這些示例代碼之后,Go 編譯器會(huì)報(bào)下面這個(gè)錯(cuò)誤:
$go run call_init.go
./call_init.go:10:2: undefined: init
接著,我們將代碼修改如下:
package mainimport "fmt"func init() {fmt.Println("init invoked")
}func main() {fmt.Println("this is main")
}
Go 編譯器運(yùn)行結(jié)果如下:
init invoked
this is main
我們看到,在初始化 Go 包時(shí),Go 會(huì)按照一定的次序,逐一、順序地調(diào)用這個(gè)包的 init 函數(shù)。一般來(lái)說(shuō),先傳遞給 Go 編譯器的源文件中的 init 函數(shù),會(huì)先被執(zhí)行;而同一個(gè)源文件中的多個(gè) init 函數(shù),會(huì)按聲明順序依次執(zhí)行。所以說(shuō),在Go中,main.main
函數(shù)可能并不是第一個(gè)被執(zhí)行的函數(shù)。
六、Go 包的初始化次序
6.1 包的初始化次序探究
我們從程序邏輯結(jié)構(gòu)角度來(lái)看,Go 包是程序邏輯封裝的基本單元,每個(gè)包都可以理解為是一個(gè)“自治”的、封裝良好的、對(duì)外部暴露有限接口的基本單元。一個(gè) Go 程序就是由一組包組成的,程序的初始化就是這些包的初始化。每個(gè) Go 包還會(huì)有自己的依賴包、常量、變量、init
函數(shù)(其中 main
包有 main
函數(shù))等。
在平時(shí)開(kāi)發(fā)中,我們?cè)陂喿x和理解代碼的時(shí)候,需要知道這些元素在在程序初始化過(guò)程中的初始化順序,這樣便于我們確定在某一行代碼處這些元素的當(dāng)前狀態(tài)。
下面,我們就通過(guò)一張流程圖,來(lái)了解 Go 包的初始化次序:
這里,我們來(lái)看看具體的初始化步驟。
首先,main
包依賴 pkg1
和 pkg4
兩個(gè)包,所以第一步,Go 會(huì)根據(jù)包導(dǎo)入的順序,先去初始化 main 包的第一個(gè)依賴包 pkg1。
第二步,Go 在進(jìn)行包初始化的過(guò)程中,會(huì)采用“深度優(yōu)先”的原則,遞歸初始化各個(gè)包的依賴包。在上圖里,pkg1 包依賴 pkg2 包,pkg2 包依賴 pkg3 包,pkg3 沒(méi)有依賴包,于是 Go 在 pkg3 包中按照“常量 -> 變量 -> init 函數(shù)”的順序先對(duì) pkg3 包進(jìn)行初始化;
緊接著,在 pkg3
包初始化完畢后,Go 會(huì)回到 pkg2
包并對(duì) pkg2 包進(jìn)行初始化,接下來(lái)再回到 pkg1
包并對(duì) pkg1
包進(jìn)行初始化。在調(diào)用完 pkg1
包的 init
函數(shù)后,Go 就完成了 main
包的第一個(gè)依賴包 pkg1
的初始化。
接下來(lái),Go 會(huì)初始化 main
包的第二個(gè)依賴包 pkg4
,pkg4
包的初始化過(guò)程與 pkg1
包類似,也是先初始化它的依賴包 pkg5
,然后再初始化自身;
然后,當(dāng) Go 初始化完 pkg4
包后也就完成了對(duì) main
包所有依賴包的初始化,接下來(lái)初始化 main
包自身。
最后,在 main 包中,Go 同樣會(huì)按照“常量 -> 變量 -> init 函數(shù)”的順序進(jìn)行初始化,執(zhí)行完這些初始化工作后才正式進(jìn)入程序的入口函數(shù) main
函數(shù)。
現(xiàn)在,我們可以通過(guò)一段代碼示例來(lái)驗(yàn)證一下 Go 程序啟動(dòng)后,Go 包的初始化次序是否是正確的,示例程序的結(jié)構(gòu)如下:
prog-init-order
├── main.go
├── pkg1
│ └── pkg1.go
├── pkg2
│ └── pkg2.go
└── pkg3└── pkg3.go
這里我只列出了 main
包的代碼,pkg1
、pkg2
和 pkg3
可可以到代碼倉(cāng)庫(kù)中查看。
package mainimport ("fmt"_ "gitee.com/tao-xiaoxin/study-basic-go/syntax/prog-init-order/pkg1"_ "gitee.com/tao-xiaoxin/study-basic-go/syntax/prog-init-order/pkg2"
)var (_ = constInitCheck()v1 = variableInit("v1")v2 = variableInit("v2")
)const (c1 = "c1"c2 = "c2"
)func constInitCheck() string {if c1 != "" {fmt.Println("main: const c1 has been initialized!")}if c2 != "" {fmt.Println("main: const c2 has been initialized!")}return ""
}func variableInit(name string) string {fmt.Printf("main: var %s has been initialized\n", name)return name
}func init() {fmt.Println("main: first init function invoked")
}func init() {fmt.Println("main: second init function invoked")
}func main() {//
}
我們可以看到,在 main 包中其實(shí)并沒(méi)有使用 pkg1 和 pkg2 中的函數(shù)或方法,而是直接通過(guò)空導(dǎo)入的方式“觸發(fā)”pkg1 包和 pkg2 包的初始化(pkg1
包和和 pkg2
包都通過(guò)空導(dǎo)入的方式依賴 pkg3 包的,),下面是這個(gè)程序的運(yùn)行結(jié)果:
$go run main.go
pkg3: const c has been initialized
pkg3: var v has been initialized
pkg3: init func invoked
pkg1: const c has been initialized
pkg1: var v has been initialized
pkg1: init func invoked
pkg2: const c has been initialized
pkg2: var v has been initialized
pkg2: init func invoked
main: const c1 has been initialized
main: const c2 has been initialized
main: var v1 has been initialized
main: var v2 has been initialized
main: first init func invoked
main: second init func invoked
正如我們預(yù)期的那樣,Go 運(yùn)行時(shí)是按照“pkg3 -> pkg1 -> pkg2 -> main
”的順序,來(lái)對(duì) Go 程序的各個(gè)包進(jìn)行初始化的,而在包內(nèi),則是以“常量 -> 變量 -> init 函數(shù)”的順序進(jìn)行初始化。此外,main 包的兩個(gè) init 函數(shù),會(huì)按照在源文件 main.go
中的出現(xiàn)次序進(jìn)行調(diào)用。根據(jù) Go 語(yǔ)言規(guī)范,**一個(gè)被多個(gè)包依賴的包僅會(huì)初始化一次,**因此這里的 pkg3 包僅會(huì)被初始化了一次。
6.2 包的初始化原則
根據(jù)以上,包的初始化按照依賴關(guān)系的順序執(zhí)行,遵循以下規(guī)則:
- 依賴包按照 “深度優(yōu)先” 的方式進(jìn)行初始化,即先初始化最底層的依賴包。
- 在每個(gè)包內(nèi)部以“常量 -> 變量 -> init 函數(shù)”的順序進(jìn)行初始化。
- 包內(nèi)的多個(gè)
init
函數(shù)按照它們?cè)诖a中的出現(xiàn)順序依次自動(dòng)調(diào)用。
七、init 函數(shù)的常用用途
Go 包初始化時(shí),init
函數(shù)的初始化次序在變量之后,這給了開(kāi)發(fā)人員在 init
函數(shù)中對(duì)包級(jí)變量進(jìn)行進(jìn)一步檢查與操作的機(jī)會(huì)。
7.1 用途一:重置包級(jí)變量值
init
函數(shù)就好比 Go 包真正投入使用之前唯一的“質(zhì)檢員”,負(fù)責(zé)對(duì)包內(nèi)部以及暴露到外部的包級(jí)數(shù)據(jù)(主要是包級(jí)變量)的初始狀態(tài)進(jìn)行檢查。在 Go 標(biāo)準(zhǔn)庫(kù)中,我們能發(fā)現(xiàn)很多 init
函數(shù)被用于檢查包級(jí)變量的初始狀態(tài)的例子,標(biāo)準(zhǔn)庫(kù) flag 包對(duì) init 函數(shù)的使用就是其中的一個(gè),這里我們簡(jiǎn)單來(lái)分析一下。
flag
包定義了一個(gè)導(dǎo)出的包級(jí)變量 CommandLine
,如果用戶沒(méi)有通過(guò) flag.NewFlagSet
創(chuàng)建新的代表命令行標(biāo)志集合的實(shí)例,那么 CommandLine
就會(huì)作為 flag
包各種導(dǎo)出函數(shù)背后,默認(rèn)的代表命令行標(biāo)志集合的實(shí)例。
而在 flag 包初始化的時(shí)候,由于 init 函數(shù)初始化次序在包級(jí)變量之后,因此包級(jí)變量 CommandLine
會(huì)在 init
函數(shù)之前被初始化了,可以看如下代碼:
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)func NewFlagSet(name string, errorHandling ErrorHandling) *FlagSet {f := &FlagSet{name: name,errorHandling: errorHandling,}f.Usage = f.defaultUsagereturn f
}func (f *FlagSet) defaultUsage() {if f.name == "" {fmt.Fprintf(f.Output(), "Usage:\n")} else {fmt.Fprintf(f.Output(), "Usage of %s:\n", f.name)}f.PrintDefaults()
}
我們可以看到,在通過(guò) NewFlagSet
創(chuàng)建 CommandLine
變量綁定的 FlagSet
類型實(shí)例時(shí),CommandLine
的 Usage
字段被賦值為 defaultUsage
。
也就是說(shuō),如果保持現(xiàn)狀,那么使用 flag
包默認(rèn) CommandLine
的用戶就無(wú)法自定義 usage
的輸出了。于是,flag
包在 init
函數(shù)中重置了 CommandLine
的 Usage
字段:
func init() {CommandLine.Usage = commandLineUsage // 重置CommandLine的Usage字段
}func commandLineUsage() {Usage()
}var Usage = func() {fmt.Fprintf(CommandLine.Output(), "Usage of %s:\n", os.Args[0])PrintDefaults()
}
這個(gè)時(shí)候我們會(huì)發(fā)現(xiàn),CommandLine
的 Usage
字段,設(shè)置為了一個(gè) flag
包內(nèi)的未導(dǎo)出函數(shù) commandLineUsage
,后者則直接使用了 flag
包的另外一個(gè)導(dǎo)出包變量 Usage
。這樣,就可以通過(guò) init
函數(shù),將 CommandLine
與包變量 Usage
關(guān)聯(lián)在一起了。
然后,當(dāng)用戶將自定義的 usage
賦值給了 flag.Usage
后,就相當(dāng)于改變了默認(rèn)代表命令行標(biāo)志集合的 CommandLine
變量的 Usage
。這樣當(dāng) flag
包完成初始化后,CommandLine
變量便處于一個(gè)合理可用的狀態(tài)了。
7.2 用途二:實(shí)現(xiàn)對(duì)包級(jí)變量的復(fù)雜初始化
有些包級(jí)變量需要一個(gè)比較復(fù)雜的初始化過(guò)程。有些時(shí)候,使用它的類型零值(每個(gè) Go 類型都具有一個(gè)零值定義)或通過(guò)簡(jiǎn)單初始化表達(dá)式不能滿足業(yè)務(wù)邏輯要求,而 init
函數(shù)則非常適合完成此項(xiàng)工作。標(biāo)準(zhǔn)庫(kù) http
包中就有這樣一個(gè)典型示例:
var (http2VerboseLogs bool // 初始化時(shí)默認(rèn)值為falsehttp2logFrameWrites bool // 初始化時(shí)默認(rèn)值為falsehttp2logFrameReads bool // 初始化時(shí)默認(rèn)值為falsehttp2inTests bool // 初始化時(shí)默認(rèn)值為false
)func init() {e := os.Getenv("GODEBUG")if strings.Contains(e, "http2debug=1") {http2VerboseLogs = true // 在init中對(duì)http2VerboseLogs的值進(jìn)行重置}if strings.Contains(e, "http2debug=2") {http2VerboseLogs = true // 在init中對(duì)http2VerboseLogs的值進(jìn)行重置http2logFrameWrites = true // 在init中對(duì)http2logFrameWrites的值進(jìn)行重置http2logFrameReads = true // 在init中對(duì)http2logFrameReads的值進(jìn)行重置}
}
我們可以看到,標(biāo)準(zhǔn)庫(kù) http
包定義了一系列布爾類型的特性開(kāi)關(guān)變量,它們默認(rèn)處于關(guān)閉狀態(tài)(即值為 false
),但我們可以通過(guò) GODEBUG
環(huán)境變量的值,開(kāi)啟相關(guān)特性開(kāi)關(guān)。
可是這樣一來(lái),簡(jiǎn)單地將這些變量初始化為類型零值,就不能滿足要求了,所以 http
包在 init
函數(shù)中,就根據(jù)環(huán)境變量 GODEBUG
的值,對(duì)這些包級(jí)開(kāi)關(guān)變量進(jìn)行了復(fù)雜的初始化,從而保證了這些開(kāi)關(guān)變量在 http
包完成初始化后,可以處于合理狀態(tài)。
7.3 用途三:在 init 函數(shù)中實(shí)現(xiàn)“注冊(cè)模式”
首先我們來(lái)看一段使用 lib/pq
包訪問(wèn) PostgreSQL 數(shù)據(jù)庫(kù)的代碼示例:
import ("database/sql"_ "github.com/lib/pq"
)func main() {db, err := sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=verify-full")if err != nil {log.Fatal(err)}age := 21rows, err := db.Query("SELECT name FROM users WHERE age = $1", age)...
}
其實(shí),這是一段“神奇”的代碼。你可以看到示例代碼是以空導(dǎo)入的方式導(dǎo)入 lib/pq
包的,main
函數(shù)中沒(méi)有使用 pq
包的任何變量、函數(shù)或方法,這樣就實(shí)現(xiàn)了對(duì) PostgreSQL 數(shù)據(jù)庫(kù)的訪問(wèn)。而這一切的奧秘,全在 pq
包的 init
函數(shù)中:
func init() {sql.Register("postgres", &Driver{})
}
這個(gè)奧秘就在,我們其實(shí)是利用了用空導(dǎo)入的方式導(dǎo)入 lib/pq
包時(shí)產(chǎn)生的一個(gè)“副作用”,也就是 lib/pq
包作為 main 包的依賴包,它的 init
函數(shù)會(huì)在 pq
包初始化的時(shí)候得以執(zhí)行。
從上面的代碼中,我們可以看到在 pq
包的 init
函數(shù)中,pq
包將自己實(shí)現(xiàn)的 SQL 驅(qū)動(dòng)注冊(cè)到了 database/sql
包中。這樣只要應(yīng)用層代碼在 Open 數(shù)據(jù)庫(kù)的時(shí)候,傳入驅(qū)動(dòng)的名字(這里是“postgres”),那么通過(guò) sql.Open
函數(shù),返回的數(shù)據(jù)庫(kù)實(shí)例句柄對(duì)數(shù)據(jù)庫(kù)進(jìn)行的操作,實(shí)際上調(diào)用的都是 pq
包中相應(yīng)的驅(qū)動(dòng)實(shí)現(xiàn)。
實(shí)際上,這種通過(guò)在 init 函數(shù)中注冊(cè)自己的實(shí)現(xiàn)的模式,就有效降低了 Go 包對(duì)外的直接暴露,尤其是包級(jí)變量的暴露,從而避免了外部通過(guò)包級(jí)變量對(duì)包狀態(tài)的改動(dòng)。
另外,從標(biāo)準(zhǔn)庫(kù) database/sql
包的角度來(lái)看,這種“注冊(cè)模式”實(shí)質(zhì)是一種工廠設(shè)計(jì)模式的實(shí)現(xiàn),sql.Open
函數(shù)就是這個(gè)模式中的工廠方法,它根據(jù)外部傳入的驅(qū)動(dòng)名稱“生產(chǎn)”出不同類別的數(shù)據(jù)庫(kù)實(shí)例句柄。
這種“注冊(cè)模式”在標(biāo)準(zhǔn)庫(kù)的其他包中也有廣泛應(yīng)用,比如說(shuō),使用標(biāo)準(zhǔn)庫(kù) image
包獲取各種格式圖片的寬和高:
package mainimport ("fmt""image"_ "image/gif" // 以空導(dǎo)入方式注入gif圖片格式驅(qū)動(dòng)_ "image/jpeg" // 以空導(dǎo)入方式注入jpeg圖片格式驅(qū)動(dòng)_ "image/png" // 以空導(dǎo)入方式注入png圖片格式驅(qū)動(dòng)"os"
)func main() {// 支持png, jpeg, gifwidth, height, err := imageSize(os.Args[1]) // 獲取傳入的圖片文件的寬與高if err != nil {fmt.Println("get image size error:", err)return}fmt.Printf("image size: [%d, %d]\n", width, height)
}func imageSize(imageFile string) (int, int, error) {f, _ := os.Open(imageFile) // 打開(kāi)圖文文件defer f.Close()img, _, err := image.Decode(f) // 對(duì)文件進(jìn)行解碼,得到圖片實(shí)例if err != nil {return 0, 0, err}b := img.Bounds() // 返回圖片區(qū)域return b.Max.X, b.Max.Y, nil
}
你可以看到,上面這個(gè)示例程序支持 png、jpeg、gif 三種格式的圖片,而達(dá)成這一目標(biāo)的原因,正是 image/png
、image/jpeg
和 image/gif
包都在各自的 init
函數(shù)中,將自己“注冊(cè)”到 image
的支持格式列表中了,你可以看看下面這個(gè)代碼:
// $GOROOT/src/image/png/reader.go
func init() {image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}// $GOROOT/src/image/jpeg/reader.go
func init() {image.RegisterFormat("jpeg", "\xff\xd8", Decode, DecodeConfig)
}// $GOROOT/src/image/gif/reader.go
func init() {image.RegisterFormat("gif", "GIF8?a", Decode, DecodeConfig)
}
那么,現(xiàn)在我們了解了 init
函數(shù)的常見(jiàn)用途。init
函數(shù)之所以可以勝任這些工作,恰恰是因?yàn)樗?Go 應(yīng)用初始化次序中的特殊“位次”,也就是 main
函數(shù)之前,常量和變量初始化之后。