# main.main 函数:Go 应用的入口函数
Go 语言中有一个特殊的函数:main 包中的 main 函数,也就是 main.main,它是所有 Go 可执行程序的用户层执行逻辑的入口函数。Go 程序在用户层面的执行逻辑,会在这个函数内按照它的调用顺序展开。
main 函数的函数原型是这样的:
package main
func main() {
// 用户层执行逻辑
... ...
}
你会发现,main 函数的函数原型非常简单,没有参数也没有返回值。而且,Go 语言要求:可执行程序的 main 包必须定义 main 函数,否则 Go 编译器会报错。在启动了多个 Goroutine(Go 语言的轻量级用户线程,后面我们会详细讲解)的 Go 应用中,main.main 函数将在 Go 应用的主 Goroutine 中执行。
不过很有意思的是,在多 Goroutine 的 Go 应用中,相较于 main.main 作为 Go 应用的入口,main.main 函数返回的意义其实更大,因为 main 函数返回就意味着整个 Go 程序的终结,而且你也不用管这个时候是否还有其他子 Goroutine 正在执行。
另外还值得我们注意的是,除了 main 包外,其他包也可以拥有自己的名为 main 的函数或方法。但按照 Go 的可见性规则(小写字母开头的标识符为非导出标识符),非 main 包中自定义的 main 函数仅限于包内使用,就像下面代码这样,这是一段在非 main 包中定义 main 函数的代码片段:
package pkg1
import "fmt"
func Main() {
main()
}
func main() {
fmt.Println("main func for pkg1")
}
你可以看到,这里 main 函数就主要是用来在包 pkg1 内部使用的,它是没法在包外使用的。
好,现在我们已经了解了 Go 应用的入口函数 main.main 的特性。不过对于 main 包的 main 函数来说,你还需要明确一点,就是它虽然是用户层逻辑的入口函数,但它却不一定是用户层第一个被执行的函数。
这是为什么呢?这跟 Go 语言的另一个函数 init 有关。
# init 函数:Go 包的初始化函数
除了前面讲过的 main.main 函数之外,Go 语言还有一个特殊函数,它就是用于进行包初始化的 init 函数了。
和 main.main 函数一样,init 函数也是一个无参数无返回值的函数:
func init() {
// 包初始化逻辑
... ...
}
那我们现在回到前面这个“main 函数不一定是用户层第一个被执行的函数”的问题,其实就是因为,如果 main 包依赖的包中定义了 init 函数,或者是 main 包自身定义了 init 函数,那么 Go 程序在这个包初始化的时候,就会自动调用它的 init 函数,因此这些 init 函数的执行就都会发生在 main 函数之前。
不过对于 init 函数来说,我们还需要注意一点,就是在 Go 程序中我们不能手工显式地调用 init,否则就会收到编译错误,就像下面这个示例,它表示的手工显式调用 init 函数的错误做法:
package main
import "fmt"
func init() {
fmt.Println("init invoked")
}
func main() {
init()
}
这样,在构建并运行上面这些示例代码之后,Go 编译器会报下面这个错误:
$go run call_init.go
./call_init.go:10:2: undefined: init
实际上,Go 包可以拥有不止一个 init 函数,每个组成 Go 包的 Go 源文件中,也可以定义多个 init 函数。
所以说,在初始化 Go 包时,Go 会按照一定的次序,逐一、顺序地调用这个包的 init 函数。一般来说,先传递给 Go 编译器的源文件中的 init 函数,会先被执行;而同一个源文件中的多个 init 函数,会按声明顺序依次执行。
# Go 包的初始化次序
这里,我们来看看具体的初始化步骤。
首先,main 包依赖 pkg1 和 pkg4 两个包,所以第一步,Go 会根据包导入的顺序,先去初始化 main 包的第一个依赖包 pkg1。
第二步,Go 在进行包初始化的过程中,会采用“深度优先”的原则,递归初始化各个包的依赖包。在上图里,pkg1 包依赖 pkg2 包,pkg2 包依赖 pkg3 包,pkg3 没有依赖包,于是 Go 在 pkg3 包中按照“常量 -> 变量 -> init 函数”的顺序先对 pkg3 包进行初始化;
紧接着,在 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 函数。
简而言之,记住 Go 包的初始化次序并不难,你只需要记住这三点就可以了:
依赖包按“深度优先”的次序进行初始化;
每个包内按以“常量 -> 变量 -> init 函数”的顺序进行初始化;
包内的多个 init 函数按出现次序进行自动调用。
# init 函数的用途
其实,init 函数的这些常用用途,与 init 函数在 Go 包初始化过程中的次序密不可分。我们前面讲过,Go 包初始化时,init 函数的初始化次序在变量之后,这给了开发人员在 init 函数中对包级变量进行进一步检查与操作的机会。
这里我们先来看 init 函数的第一个常用用途:重置包级变量值。
init 函数就好比 Go 包真正投入使用之前唯一的“质检员”,负责对包内部以及暴露到外部的包级数据(主要是包级变量)的初始状态进行检查。在 Go 标准库中,我们能发现很多 init 函数被用于检查包级变量的初始状态的例子,标准库 flag 包对 init 函数的使用就是其中的一个,这里我们简单来分析一下。
flag 包定义了一个导出的包级变量 CommandLine,如果用户没有通过 flag.NewFlagSet 创建新的代表命令行标志集合的实例,那么 CommandLine 就会作为 flag 包各种导出函数背后,默认的代表命令行标志集合的实例。
而在 flag 包初始化的时候,由于 init 函数初始化次序在包级变量之后,因此包级变量 CommandLine 会在 init 函数之前被初始化了。
init 函数的第二个常用用途,是实现对包级变量的复杂初始化。