环境OS:Ubuntu20.04.2LTS;x86_64Go:goversiongo1.16.2linux/amd64包初始化初始化函数和其他普通函数一样,属于定义它的包,以下统称为当前包。一般来说,一个包的初始化过程分为三个步骤:初始化当前包所依赖的所有包,包括依赖包的依赖包。用初始值初始化当前包的所有全局变量。执行当前包的所有初始化函数。此过程将在本文中详细描述。基本定义Golang中有一类特殊的初始化函数,其定义格式如下:packagepkgfuncinit(){//todosth}初始化函数的一个特殊之处在于它在可执行文件的main入口函数之前自动执行程序执行完毕,不能直接调用!初始化函数的重复声明第二个特点是:在同一个包下,可以多次定义。普通函数在同一个包下不能重名,否则会变异失败:xxxredeclaredinthisblock。编译重命名初始化函数的第三个特点是编译重命名规则不同于普通函数。一般函数在编译时的重命名规则是“[模块名].包名.函数名”。虽然在源码中初始化函数的名字是init,但是编译时的重命名规则是“[模块名].包名.init.number后缀”。例如:上面的func_init.0.go源文件编译后,init函数更名为:main.init.0。上面的func_init.1.go源文件编译完成后,两个init函数分别重命名为:main.init.0、main.init.1。如上图,如果同一个包下有多个init函数,重命名时后缀数字会依次递增1。为什么会这样?那是因为Golang编译器对init函数做了特殊处理,相关源码位于cmd/compile/internal/gc/init.go文件中。全局变量renameinitgen用于记录当前包名下的init函数个数和下一个init函数的后缀值。每当Golang编译器遇到名为init的函数时,它都会调用一次renameinit()函数,最终init函数变得不可调用。为什么要重命名init函数?正如我们在上面看到的,init函数可以在同一个包下重复声明,这可能是重命名的原因。随着我们不断探索,我们可能会离真相越来越近。有一点需要明确并始终坚信:除了全局常量和全局变量的声明外,所有可执行代码都必须在函数内部执行。通常,在编译代码后,声明的全局常量可能存储在可执行文件的.rodata部分中。声明的全局变量可以存储在可执行文件的.data、.bss、.noptrdata等部分中。声明的函数或方法被编译成存储在可执行文件的.text部分中的机器指令。那么,在下面的代码(func_init.go)中,如何在声明全局变量并进行初始化赋值的同时进行编译呢?以下代码是变量声明。varmvarname及后面的代码包含函数调用和初始化赋值,最终编译成机器指令,需要在main函数之前执行;这些指令最终必须占用一个存储空间并能够被加载到内存中。varm=map[string]int{"Jack":18,"Rose":16,}varname=flag.String("name","","username")它们存储在可执行文件的什么位置??通过逆向分析,发现Go编译器合并了函数外的代码调用(全局变量的初始化赋值),自动生成了一个init函数;显然,初始化函数并没有在func_init.go源文件中定义。这也可能是编译器重命名自定义init函数的原因。编译器存储所有不能直接调用的初始化函数!所有这些都将在程序启动时自动存储和执行。在代码编译过程中,当前包的初始化函数及其依赖包的初始化会保存在一个特殊的结构体中,定义在runtime/proc.go源文件中,如下:typeinitTaskstruct{stateuintptr//初始化状态程序运行时当前包的个数:0=uninitialized,1=inprogress,2=donendepsuintptr//当前包的依赖包个数nfnsuintptr//当前包的初始化函数个数}Go语言是一门很语法糖对于繁重的编程语言,您在源代码中看到的往往不是真实的。runtime.initTask结构是一个动态结构,可以在编译时修改。它的真实样子是这样的:typeinitTaskstruct{stateuintptr//程序运行时当前包的初始化状态:0=未初始化,1=进行中,2=donendepsuintptr//当前包的依赖包个数nfnsuintptr//当前包的初始化函数数量deps[ndeps]*initTask//当前包(非slice)依赖包的InitTask指针数组fns[nfns]func()//当前包(非slice)的初始化函数指针数组}每个包的依赖包个数可能不同(ndeps),每个包的初始化函数个数不同(nfns),所以最终生成的initTask对象的大小可能不同。具体编译过程参考cmd/compile/internal/gc/init.go源文件中的fninit函数,这里不再赘述。Go编译器为每个包生成一个runtime.initTask类型的全局变量。这个变量的命名规则是“packagename..inittask”,如下图:从上图第三列可以看出,每个package的initTask对象大小不一。具体计算方法如下:size:=(3+ndeps+nfns)*8初始化过程在可执行程序启动的初始化过程中,先初始化运行包及其依赖包,再初始化主包及其依赖包被初始化。一个包可能依赖于多个包,但每个包只初始化一次,由runtime.initTask.state字段控制。具体的初始化逻辑可以参考runtime/proc.go源文件中的main函数和doInit函数。在初始化过程中,会多次调用runtime.doInit函数,其具体执行流程与本文开头“包初始化”一节中描述的一致。上图所示的func_init.2.go源文件包含两个编译后的初始化函数:一个是编译器自动生成的,一个是编译器重命名的;首先执行自动生成的初始化函数。如上图所示的func_init.2.go源文件,编译后生成的main..inittask全局变量内存地址为0x000000000054dc60。我们动态调试runtime.doInit函数,当其参数为main..inittask全局变量指针时暂停执行,观察参数的数据结构。从动态调试时显示的内存数据,我们推导出如下伪代码:thatthecurrentpackagedependsinthenumberofinitTasksnfnsuintptr//当前包的初始化函数个数deps[2]uintptr//当前包所依赖的包的InitTask指针数组(不是slice)fns[2]uintptr//当前包(不是slice)的初始化函数指针数组}{state:0,ndeps:2,nfns:2,deps:[2]uintptr{0x54ef60,0x54eca0},//flag..inittask,fmt..inittaskfns:[2]uintptr{0x4a4ec0,0x4a4d60},//main.init,main.init.0}在func_init.2.go源码文件中,引用了flag和fmt两个包,所以必须对main包进行初始化这两个包的初始化完成后执行。import"flag"import"fmt"通常initTask.ndeps字段的值与导入次数相同。编译器自动生成的init函数在代码源文件中的自定义init函数之前执行。结束语至此,本文已经比较完整、详细地介绍了Go中初始化函数相关的内容。相信在仔细分析了初始化函数的所有细节之后,我对Go有了更深入的了解。希望能帮助减少开发和编码过程中的疑惑,让它变得更加方便和轻松。本文转载自微信公众号「记忆中的Golang」
