在 golang 中,可执行文档的入口函数并不是我们写的 main 函数,编译器在编译 go 代码时会插入一段起引导作用的汇编代码,它引导进程进行命令行参数、运行时的初始化,例如内存分配器初始化、垃圾回收器初始化、协程调度器的初始化。golang 引导初始化之后就会进入用户逻辑,因为存在特殊的 init 函数,main 函数也不是进程最开始执行的函数。

一、golang 进程启动流程

  golang 可执行进程由于运行时 runtime 的存在,其启动过程还是非常复杂的,这里通过 gdb 调试工具简单查看其启动流程:

1
2
3
4
5
6
7
CALL   runtime·args(SB)
CALL runtime·osinit(SB)
CALL runtime·schedinit(SB)

CALL runtime·newproc(SB)

CALL runtime·mstart(SB)

二、特殊的 init 函数

  1. init 函数先于 main 函数自动执行,不能被其他函数调用
  2. init 函数没有输入参数、没有返回值
  3. 每个包可以含有多个同名的 init 函数,每个源文档也可以有多个同名的 init 函数
  4. 执行顺序 变量初始化 > init 函数 > main 函数。在复杂项目中,初始化的顺序如下:
    • 先初始化 import 包的变量,然后先初始化 import 的包中的 init 函数,,再初始化 main 包变量,最后执行 main 包的 init 函数
    • 从上到下初始化导入的包(执行 init 函数),遇到依赖关系,先初始化没有依赖的包
    • 从上到下初始化导入包中的变量,遇到依赖,先执行没有依赖的变量初始化
    • main 包本身变量的初始化,main 包本身的 init 函数
    • 同一个包中不同源文档的初始化是按照源文档名称的字典序

util.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package util

import (
"fmt"
)

var c int = func() int {
fmt.Println("util variable init")
return 3
}()

func init() {
fmt.Println("call util.init")
}

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
_ "util"
)

var a int = func() int {
fmt.Println("main variable init")
return 3
}()

func init() {
fmt.Println("call main.init")
}

func main() {
fmt.Println("call main.main")
}

执行结果:
   util variable init
   call util.init
   main variable init
   call main.init
   call main.main

参考:《Go 语言学习笔记 13、14、15 章》