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

开发 前端
本文完整地、详细地介绍了Go中关于初始化函数相关的内容。相信在认真刨析了初始化函数的所有细节之后,对Go有了更近一步的了解。

[[425952]]

环境

  1. OS : Ubuntu 20.04.2 LTS; x86_64 
  2. Go : go version go1.16.2 linux/amd64 

包初始化

初始化函数与其他普通函数一样,都隶属于定义它的包(package),以下统称为当前包。

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

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

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

基本定义

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

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

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

重复声明

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

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

编译重命名

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

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

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

例如:

  • 在上述的 func_init.0.go 源文件编译之后,init函数被重命名为:main.init.0。
  • 在上述的 func_init.1.go 源文件编译之后,两个init函数分别被重命名为:main.init.0、main.init.1。

如上所示,如果同一个包下有多个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
相关推荐

2019-08-06 09:00:00

JavaScript函数式编程前端

2023-11-20 08:18:49

Netty服务器

2023-11-06 08:16:19

APM系统运维

2022-11-11 19:09:13

架构

2023-10-27 08:15:45

2022-02-24 07:34:10

SSL协议加密

2023-11-08 08:15:48

服务监控Zipkin

2024-03-26 00:17:51

Go语言IO

2020-02-02 15:14:24

HTTP黑科技前端

2022-04-28 09:22:46

Vue灰度发布代码

2020-10-08 14:32:57

大数据工具技术

2022-09-29 13:09:38

DataClassPython代码

2024-04-26 00:01:00

Go语言类型

2019-07-04 15:16:52

数据挖掘大数据算法

2018-10-22 08:14:04

2023-03-31 08:16:53

Flutter优化内存管理

2023-12-06 16:28:56

2022-09-06 11:21:49

光网络光纤

2022-02-18 10:13:07

SolrElasticSea开源

2021-09-28 07:12:10

avaScriptCurrying柯里化
点赞
收藏

51CTO技术栈公众号