文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

一文带你了解【Go】初始化函数

2024-12-02 20:50

关注

一般来讲,一个包初始化过程分三步:

  1. 初始化当前包依赖的所有包,包括依赖包的依赖包。
  2. 初始化当前包所有具有初始值的全局变量。
  3. 执行当前包的所有初始化函数。

关于这个过程,本文会一一详细介绍。

基本定义

在Golang中有一类特殊的初始化函数,其定义格式如下:

  1. package pkg 
  2.  
  3. func init() { 
  4.   // to do sth 

初始化函数一个特殊之处是:其在可执行程序的main入口函数执行之前自动执行,而且不可被直接调用!

重复声明

初始化函数第二个特殊之处是:在同一个包下,可以重复定义多次。

普通函数在同一个包下不可以重名,否则变异失败:xxx redeclared in this block。

编译重命名

初始化函数第三个特殊之处是:编译重命名规则与普通函数不同。

普通函数在编译过程中一般重命名规则为“[模块名].包名.函数名”。

初始化函数在源码中虽然名称为init,但在编译过程中重命名规则为“[模块名].包名.init.数字后缀”。

例如:

如上所示,如果同一个包下有多个init函数,重命名时后缀数字按顺序增加一。

为什么会这样呢?

那是因为Golang编译器对 init 函数进行了特殊处理,相关源码位于 cmd/compile/internal/gc/init.go 文件中。

全局变量 renameinitgen 用于记录当前包名下init函数的数量以及下一个init函数后缀的值。

每当Golang编译器遇到一个名称为 init 的函数,就会调用一次 renameinit() 函数,最终 init 函数变得不可被调用。

为什么重命名init函数?

如上述我们看到的,在同一个包下可以重复声明 init 函数,这可能是需要重命名的原因。

当我们继续探究时,可能更加接近真相。

有一点需要明确并始终坚信:除全局常量和全局变量的声明之外,所有的可执行代码都必须在函数内执行。

通常情况下,代码编译之后,

  1. 声明的全局常量可能被存储在可执行文件的.rodata section。
  2. 声明的全局变量可能被存储在可执行文件的.data、.bss、.noptrdata等section。
  3. 声明的函数或方法被编译为机器指令存储在可执行文件的.text section。

那么,以下代码中(func_init.go),声明全局变量的同时进行初始化赋值,该如何编译呢? 

以下代码属于变量声明。

  1. var m 
  2. var name 

而以下代码包含函数调用和初始化赋值,最终要被编译为机器指令,并且需要在main函数之前执行;这些指令最终必须占用一块存储空间并且能够加载到内存中。

  1. var m = map[string]int
  2.     "Jack": 18, 
  3.     "Rose": 16, 
  4.  
  5. var name = flag.String("name""""user name"

它们被存储在可执行文件的什么地方了呢?

通过逆向分析,发现Go编译器合并了函数外的代码调用(全局变量的初始化赋值),自动生成了一个 init 函数;很明显,在func_init.go源文件中并没有定义初始化函数。

这可能也是编译器重命名自定义init函数的原因吧。

编译存储

所有的初始化函数都不可被直接调用!所有它们会被存储起来并在程序启动时自动执行。

在代码编译过程中,当前包的初始化函数及其依赖的包的初始化,会被存储到一个特殊的结构体中,该结构体定义在runtime/proc.go源文件中,如下所示:

  1. type initTask struct { 
  2.     state uintptr // 当前包在程序运行时的初始化状态:0 = uninitialized, 1 = in progress, 2 = done 
  3.     ndeps uintptr // 当前包的依赖包的数量 
  4.     nfns  uintptr // 当前包的初始化函数数量 

Go语言是一个语法糖很重的编程语言,在源码中看到的往往不是真实的。

runtime.initTask结构体是一个编译时可修改的动态结构。其真实面貌如下所示:

  1. type initTask struct { 
  2.     state uintptr // 当前包在程序运行时的初始化状态:0 = uninitialized, 1 = in progress, 2 = done 
  3.     ndeps uintptr // 当前包的依赖包的数量 
  4.     nfns  uintptr // 当前包的初始化函数数量 
  5.     deps  [ndeps]*initTask // 当前包的依赖包的initTask指针数组(不是slice) 
  6.     fns   [nfns]func ()    // 当前包的初始化函数指针数组(不是slice) 

每个包的依赖包数量可能不同(ndeps),每个包的初始化函数数量不同(nfns),所以最终生成的initTask对象大小可能不同。

具体编译过程参考cmd/compile/internal/gc/init.go源文件中的fninit函数,此处不再赘述。

Go编译器为每个包生成一个runtime.initTask类型的全局变量,该变量的命名规则为“包名..inittask”,如下所示:

从上图第三列可以看出,每个包的initTask对象大小不同。具体计算方法如下:

  1. size := (3 + ndeps + nfns) * 8 

初始化过程

在可执行程序启动的初始化过程中,优先执行runtime包及其依赖包的初始化,然后执行main包及其依赖包的初始化。

一个包可能被多个包依赖,但是每个包的都只初始化一次,通过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全局变量指针时暂停执行,观察参数的数据结构。

从动态调试时展示的内存数据我们反推出如下伪代码:

  1. package main 
  2.  
  3. var inittask = struct { 
  4.   state uintptr    // 当前包在程序运行时的初始化状态:0 = uninitialized, 1 = in progress, 2 = done 
  5.   ndeps uintptr    // 当前包依赖的包的initTask数量 
  6.   nfns  uintptr    // 当前包的初始化函数数量 
  7.   deps  [2]uintptr // 当前包依赖的包的initTask指针数组(不是slice) 
  8.   fns   [2]uintptr // 当前包的初始化函数指针数组(不是slice) 
  9. }{ 
  10.   state: 0, 
  11.   ndeps: 2, 
  12.   nfns:  2, 
  13.   deps:  [2]uintptr{0x54ef60, 0x54eca0}, // flag..inittask,fmt..inittask 
  14.   fns:   [2]uintptr{0x4a4ec0, 0x4a4d60}, // main.init,main.init.0 

在func_init.2.go源文件中,引用了flag、fmt两个包,所以main包的初始化必须在这两个包的初始化完成之后执行。

  1. import "flag" 
  2. import "fmt" 

通常initTask.ndeps字段的值与import的数量相同。

编译器自动生成的init函数先于代码源文件中自定义的init函数执行。

结语

至此,本文完整地、详细地介绍了Go中关于初始化函数相关的内容。

相信在认真刨析了初始化函数的所有细节之后,对Go有了更近一步的了解。

希望有助于减少开发编码过程中的疑惑,更加得心应手,游刃有余。

本文转载自微信公众号「Golang In Memory」

 

来源:Golang In Memory内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯