Go的所有init函数会在main函数执行之前运行,init的执行是runtime帮助我们完成的,我们来看看init的运行机制。
init函数的编译处理
1 2 3 4 5 6 7 8 type initTask struct { state uintptr ndeps uintptr nfns uintptr }
initTask是init初始化工作的关键结构体,一个包会有一个对应的initTask:
state: 该包的初始化函数是否被执行
ndeps: 该包有几个依赖的包
nfns: 包中的初始化函数数量
虽然每个包都有一个对应的initTask,但是Go在编译中只会为main包生成一个initTask符号main..initTask,并将其中的init函数命名为包名.init.0,包名.init.1...,其他包的initTask的位置我们可以通过main包的initTask的位置找到,具体如何寻找的会在第二部分中说明
,我们来看一段代码
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "awesomeProject/test" "fmt" ) func init () { var a = 100 var b = 200 b = a a = b } func init () { var a = 200 fmt.Println(a) } func main () { fmt.Println(test.A) }
test/test.go
1 2 3 4 5 6 7 8 9 10 11 package testimport "fmt" var A int func init () { var b = 100 fmt.Println(b) }
我们在编译以后查看符号表
1 $ readelf -s main | grep -E 'main|test'
output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 658: 0000000000433180 816 FUNC GLOBAL DEFAULT 1 runtime.main 659: 00000000004334c0 53 FUNC GLOBAL DEFAULT 1 runtime.main.func2 822: 0000000000441780 500 FUNC GLOBAL DEFAULT 1 runtime.testAtomic64 1021: 0000000000458140 58 FUNC GLOBAL DEFAULT 1 runtime.main.func1 1300: 0000000000463560 1279 FUNC GLOBAL DEFAULT 1 strconv.roundShortest 1517: 00000000004805c0 58 FUNC GLOBAL DEFAULT 1 main.init.0 1518: 0000000000480600 179 FUNC GLOBAL DEFAULT 1 main.init.1 1519: 00000000004806c0 171 FUNC GLOBAL DEFAULT 1 main.main 1520: 0000000000514de0 56 OBJECT GLOBAL DEFAULT 9 main..inittask 1522: 0000000000558e58 8 OBJECT GLOBAL DEFAULT 12 awesomeProject/test.A 1716: 000000000052b148 8 OBJECT GLOBAL DEFAULT 11 runtime.main_ini[...] 1717: 0000000000558dec 1 OBJECT GLOBAL DEFAULT 12 runtime.mainStarted 1748: 0000000000558fb8 8 OBJECT GLOBAL DEFAULT 12 runtime.test_z64 1749: 0000000000558fb0 8 OBJECT GLOBAL DEFAULT 12 runtime.test_x64 1779: 000000000052b188 8 OBJECT GLOBAL DEFAULT 11 runtime.testSigtrap 1780: 000000000052b190 8 OBJECT GLOBAL DEFAULT 11 runtime.testSigusr1 1821: 0000000000514020 32 OBJECT GLOBAL DEFAULT 9 internal/testlog[...] 2110: 00000000004b5d20 8 OBJECT GLOBAL DEFAULT 2 runtime.mainPC
可以看到main..inittask
,main.init.0
,main.init.1
,go在执行所有包init函数的过程中,会以main包中的init为起点,通过依赖关系遍历到所有的包
深入init函数的执行流程
go version: 1.20
我们先来找到runtime中调用init的部分
runtime/proc.go
1 2 3 4 5 6 7 8 var main_inittask initTaskfunc main () { ... doInit(&main_inittask) ... }
通过观察我们发现main_inittask这个变量链接的就是main..inittask,也就是main包的inittask
现在我们已经得到了main的inittask,接下来我们深入看看doInit是如何运行的
runtime/proc.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 func doInit (t *initTask) { switch t.state { case 2 : return case 1 : throw("recursive call during initialization - linker skew" ) default : t.state = 1 for i := uintptr (0 ); i < t.ndeps; i++ { p := add(unsafe.Pointer(t), (3 +i)*goarch.PtrSize) t2 := *(**initTask)(p) doInit(t2) } if t.nfns == 0 { t.state = 2 return } ... firstFunc := add(unsafe.Pointer(t), (3 +t.ndeps)*goarch.PtrSize) for i := uintptr (0 ); i < t.nfns; i++ { p := add(firstFunc, i*goarch.PtrSize) f := *(*func () )(unsafe.Pointer(&p)) f() } ... t.state = 2 } }
在开始解读这一段代码之前,需要再次认识一次initTask结构体,你在go1.20中看到的initTask是被优化过的,实际内容如下:
1 2 3 4 5 6 7 type initTask struct { state uintptr ndeps uintptr nfns uintptr deps [ndeps]*initTask fns [nfns]func () }
通过这个展开以后的initTask和doInit函数,我们可以分析出init函数执行的流程如下:
先找到main包对应的initTask
递归先执行依赖包的initTask,因为结构体中没有显示声明出对应的字段,所以需要自己运算出字段所在的位置
1 2 3 4 5 6 7 for i := uintptr (0 ); i < t.ndeps; i++ { p := add(unsafe.Pointer(t), (3 +i)*goarch.PtrSize) t2 := *(**initTask)(p) doInit(t2) }
最后再执行自己的包中的init函数
1 2 3 4 5 6 7 8 firstFunc := add(unsafe.Pointer(t), (3 +t.ndeps)*goarch.PtrSize) for i := uintptr (0 ); i < t.nfns; i++ { p := add(firstFunc, i*goarch.PtrSize) f := *(*func () )(unsafe.Pointer(&p)) f() }