14.init你用对了吗?

本文视频地址

在 Go 语言中,通过包的 init 函数来完成初始化的工作。

1. init 函数

Go 语言中有两个特殊的函数:
1) main 包中的 main 函数,它是所有 Go 可执行程序的入口函数;
2) 包级别的 init 函数。
init 函数是一个无参无返回值的函数:

func init() {
        ...
}

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

一个 Go 包可以拥有多个 init 函数,每个组成包的源文件中亦可以定义多个 init 函数。在初始化该包时,Go 运行时会按照一定的次序逐一顺序地调用该包的 init 函数。每个 init 函数在整个 Go 程序生命周期内仅会被执行一次。因此,init 函数可以做一些包级初始化以及包级数据初始状态的检查工作。
一般来说,先被传递给 Go 编译器的源文件中的 init 函数先被执行;同一个源文件中的多个 init 函数按声明顺序依次执行。在Go 语言中:不要依赖 init 函数的执行次序。

2. 程序初始化顺序

除了 init 函数是顺序执行并仅被执行一次之外,Go 程序初始化顺序也给 init 函数提供了前提条件。
在这里插入图片描述

  • main 包依赖 pkg1、pkg4 两个包;
  • Go 运行时会根据包导入的顺序,先去初始化 main 包的第一个依赖包 pkg1;
  • Go 运行时遵循“深度优先”原则查看到:pkg1 依赖 pkg2,于是 Go 运行时去初始化 pkg2;
  • pkg2 依赖 pkg3,Go 运行时去初始化 pkg3;
  • pkg3 没有依赖包,于是 Go 运行时在 pkg3 包中按照”常量 -> 变量 -> init 函数"的顺序进行初始化;
  • pkg3 初始化完毕后,Go 运行时会回到 pkg2 并对 pkg2 进行初始化;接下来再回到 pkg1 并对 pkg1 进行初始化;
  • 在调用完 pkg1 的 init 函数后,Go 运行时完成 main 包的第一个依赖包 pkg1 的初始化;
  • Go 运行时接下来会初始化 main 包的第二个依赖包 pkg4;
  • pkg4 的初始化过程与 pkg1 类似,也是先初始化其依赖包 pkg5,然后再初始化自身;
  • 当 Go 运行时初始化完 pkg4 后,也就完成了对 main 包所有依赖包的初始化,接下来初始化 main 包自身;
  • 在 main 包中,Go 运行时会按照”常量 -> 变量 -> init 函数"的顺序进行初始化,执行完这些初始化工作后才正式进入程序的入口函数 main 函数。

init 函数适合做包级数据初始化和初始状态检查的原因就是 init 函数执行在包的包级变量之后。

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

init 函数负责对包内部以及暴露到外部的包级数据(主要是包级变量)的初始状态进行检查。

a) 重置包级变量值
// $GOROOT/src/context/context.go
// closedchan is a reusable closed channel.
var closedchan = make(chan struct{})
func init() {
        close(closedchan)
}

context 包在 cancelCtx 的 cancel 方法中需要一个可复用的、处于关闭状态的 channel,于是 context 包定义了一个未导出包级变量 closedchan 并对其进行了初始化。

b) 对包级变量进行初始化,保证其后续可用
// $GOROOT/src/net/http/h2_bundle.go
var (
        http2VerboseLogs    bool
        http2logFrameWrites bool
        http2logFrameReads  bool
        http2inTests        bool
)

func init() {
        e := os.Getenv("GODEBUG")
        if strings.Contains(e, "http2debug=1") {
                http2VerboseLogs = true
        }
        if strings.Contains(e, "http2debug=2") {
                http2VerboseLogs = true
                http2logFrameWrites = true
                http2logFrameReads = true
        }
}
c) init 函数中的“注册模式”
// github.com/lib/pq/conn.go
...
func init() {
        sql.Register("postgres", &Driver{})
}
... 

导入 lib/pq 的副作用就是 Go 运行时会将 lib/pq 作为 main 包的依赖包,Go 运行时会初始化 pq 包,pq 包的 init 函数执行。在 pq 包的 init 函数中,pq 包将自己实现的 sql 驱动(driver)注册到 sql 包中。这样只要应用层代码在 Open 数据库的时候传入驱动的名字(这里是“postgres”),那么通过 sql.Open 函数返回的数据库实例句柄对数据库进行的操作实际上调用的都是 pq 这个驱动的相应实现。

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

一旦 init 函数在检查包数据初始状态时遇到失败或错误的情况(尽管极少出现),一般建议直接调用 panic。或通过 log.Fatal 等方法记录异常日志后再调用 panic 使程序退出。

在这里插入图片描述

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 1024 设计师:白松林 返回首页