从程序逻辑结构角度来看,Go 包(package)是程序逻辑封装的基本单元,每个包都可以理解为一个”自治“的、封装良好的、对外部暴露有限接口的基本单元。一个 Go 程序就是由一组包组成的。

在 Go 包这一基本单元中分布着常量、包级变量、函数、类型和类型方法、接口等,我们要保证包内部的这些元素在被使用之前处于合理有效的初始状态,尤其是包级变量。在 Go 语言中,我们一般通过包的 init 函数来完成这一工作。

1. 认识 init 函数

init 函数是一个无参数无返回值的函数:

  1. func init() {
  2. ... ...
  3. }

如果一个包定义了 init 函数,Go 运行时会负责在该包初始化时调用它的 init 函数。在 Go 程序中我们不能显式调用 init,否则会在编译期间报错:

  1. // call_init_in_main.go
  2. package main
  3. import "fmt"
  4. func init() {
  5. fmt.Println("init invoked")
  6. }
  7. func main() {
  8. init()
  9. }
  10. $go run call_init_in_main.go
  11. # command-line-arguments
  12. ./call_init_in_main.go:10:2: undefined: init
  • 一个 Go 包可以拥有多个 init 函数,每个组成 Go 包的 Go 源文件中亦可以定义多个 init 函数。
  • 在初始化该 Go 包时,Go 运行时会按照一定的次序逐一顺序地调用该包的 init 函数。
  • Go 运行时不会并发调用 init 函数,它会等待一个 init 函数执行完毕返回后再执行下一个 init 函数,且每个 init 函数在整个 Go 程序生命周期内仅会被执行一次。
  • 因此,init 函数极其适合做一些包级数据初始化工作以及包级数据初始状态的检查工作。

一个包内的、分布在多个文件中的多个 init 函数的执行次序是什么样的呢?

  • 一般来说,先被传递给 Go 编译器的源文件中的 init 函数先被执行;同一个源文件中的多个 init 函数按声明顺序依次执行。
  • 但 Go 语言的惯例告诉我们:不要依赖 init 函数的执行次序

2. 程序初始化顺序

Go 程序由一组包组合而成,程序的初始化就是这些包的初始化。每个 Go 包都会有自己的依赖包、每个包还包含有常量、变量、init 函数(其中 main 包有 main 函数)等,这些元素在程序初始化过程中的初始化顺序是什么样的呢?我们用下面的这幅图来说明一下:

image.png

init 函数适合做包级数据初始化和初始状态检查的前提条件就是 init 函数的执行顺位排在其所在包的包级变量之后。
**
我们再通过代码示例来验证一下上述的程序启动初始化顺序:

  1. // 示例程序的结构如下:
  2. package-init-order
  3. ├── go.mod
  4. ├── main.go
  5. ├── pkg1
  6. └── pkg1.go
  7. ├── pkg2
  8. └── pkg2.go
  9. └── pkg3
  10. └── pkg3.go

包的依赖关系如下:

  • main 包依赖 pkg1 和 pkg3;
  • pkg1 依赖 pkg2。

由于篇幅所限,这里仅列出 main 包的代码,pkg1、pkg2 和 pkg3 包的代码与 main 包类似:

  1. // package-init-order/main.go
  2. package main
  3. import (
  4. "fmt"
  5. _ "github.com/bigwhite/package-init-order/pkg1"
  6. _ "github.com/bigwhite/package-init-order/pkg3"
  7. )
  8. var (
  9. _ = constInitCheck()
  10. v1 = variableInit("v1")
  11. v2 = variableInit("v2")
  12. )
  13. const (
  14. c1 = "c1"
  15. c2 = "c2"
  16. )
  17. func constInitCheck() string {
  18. if c1 != "" {
  19. fmt.Println("main: const c1 init")
  20. }
  21. if c1 != "" {
  22. fmt.Println("main: const c2 init")
  23. }
  24. return ""
  25. }
  26. func variableInit(name string) string {
  27. fmt.Printf("main: var %s init\n", name)
  28. return name
  29. }
  30. func init() {
  31. fmt.Println("main: init")
  32. }
  33. func main() {
  34. // do nothing
  35. }

我们看到 main 包并未使用 pkg1 和 pkg3 中的函数或方法,而是直接通过包的空别名方式“触发”pkg1 和 pkg3 的初始化,下面是这个程序的运行结果:

  1. $go run main.go
  2. pkg2: const c init
  3. pkg2: var v init
  4. pkg2: init
  5. pkg1: const c init
  6. pkg1: var v init
  7. pkg1: init
  8. pkg3: const c init
  9. pkg3: var v init
  10. pkg3: init
  11. main: const c1 init
  12. main: const c2 init
  13. main: var v1 init
  14. main: var v2 init
  15. main: init

3. 使用 init 函数检查包级变量的初始状态

a) 重置包级变量值

  1. // $GOROOT/src/flag/flag.go
  2. func init() {
  3. // Override generic FlagSet default Usage with call to global Usage.
  4. // Note: This is not CommandLine.Usage = Usage,
  5. // because we want any eventual call to use any updated value of Usage,
  6. // not the value it has when this line is run.
  7. CommandLine.Usage = commandLineUsage
  8. }
  1. // $GOROOT/src/context/context.go
  2. // closedchan is a reusable closed channel.
  3. var closedchan = make(chan struct{})
  4. func init() {
  5. close(closedchan)
  6. }

b) 对包级变量进行初始化,保证其后续可用

  1. // $GOROOT/src/regexp/regexp.go
  2. // Bitmap used by func special to check whether a character needs to be escaped.
  3. var specialBytes [16]byte
  4. // special reports whether byte b needs to be escaped by QuoteMeta.
  5. func special(b byte) bool {
  6. return b < utf8.RuneSelf && specialBytes[b%16]&(1<<(b/16)) != 0
  7. }
  8. func init() {
  9. for _, b := range []byte(`\.+*?()|[]{}^$`) {
  10. specialBytes[b%16] |= 1 << (b / 16)
  11. }
  12. }
  1. // $GOROOT/src/net/addrselect.go
  2. func init() {
  3. sort.Sort(sort.Reverse(byMaskLength(rfc6724policyTable)))
  4. }
  1. // $GOROOT/src/net/http/h2_bundle.go
  2. var (
  3. http2VerboseLogs bool
  4. http2logFrameWrites bool
  5. http2logFrameReads bool
  6. http2inTests bool
  7. )
  8. func init() {
  9. e := os.Getenv("GODEBUG")
  10. if strings.Contains(e, "http2debug=1") {
  11. http2VerboseLogs = true
  12. }
  13. if strings.Contains(e, "http2debug=2") {
  14. http2VerboseLogs = true
  15. http2logFrameWrites = true
  16. http2logFrameReads = true
  17. }
  18. }

c) init 函数中的“注册模式”

下面是使用lib/pq 包访问 PostgreSQL 数据库的一段代码示例:

  1. import (
  2. "database/sql"
  3. _ "github.com/lib/pq"
  4. )
  5. func main() {
  6. db, err := sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=verify-full")
  7. if err != nil {
  8. log.Fatal(err)
  9. }
  10. age := 21
  11. rows, err := db.Query("SELECT name FROM users WHERE age = $1", age)
  12. ...
  13. }

对于初学 Go 的 gopher 来说,这是一段“神奇”的代码,因为在以空别名方式导入 lib/pq 包后,main 函数中似乎并没有使用 pq 的任何变量、函数或方法。这段代码的奥秘全在 pq 包的 init 函数中:

  1. // github.com/lib/pq/conn.go
  2. ... ...
  3. func init() {
  4. sql.Register("postgres", &Driver{})
  5. }
  6. ... ...

这种通过在 init 函数中注册自己的实现的模式,降低了 Go 包对外的直接暴露,尤其是包级变量的暴露,避免了外部通过包级变量对包状态的改动。从 database/sql 的角度来看,这种“注册模式”实质是一种工厂设计模式的实现,sql.Open 函数就是该模式中的工厂方法,它根据外部传入的驱动名称“生产”出不同类别的数据库实例句柄

d) init 函数中检查失败的处理方法

init 函数是一个无参数无返回值的函数,并且它的主要目的就是保证其所在包在被正式使用之前包的初始状态是有效的。一旦 init 函数在检查包数据初始状态时遇到失败或错误的情况(尽管极少出现),则说明对包的“质检”亮了红灯,如果让包“出厂”,那么只会导致更为严重的影响。因此,在这种情况下,快速失败是最佳选择。我们一般建议直接调用 panic。或通过 log.Fatal 等方法记录异常日志后再调用 panic 使程序退出。