6.4 赋值语句

对于全局变量的赋值语句,编译器需要确定其初始化的顺序及初始化时间,我们分别加以讨论。

初始化顺序

确定初始化顺序的逻辑在文件cmd/compile/internal/pkginit/initorder.go中,入口函数是initOrder(), 该函数在总体逻辑中的 Step 2 中被调用。

影响初始化顺序的因素有两个:一是依赖关系、二是申明顺序。例如如下代码:

var nameAlias string = name + "!"
var name string = getDefaultName()
var version string = getDefaultVersion()

func getDefaultName() string {
	return "Golang"
}

func getDefaultVersion() string {
	return "1.17"
}

其中 nameAlias 依赖于 name, 所以即使它声明在 name 前面,其也应该在 name 之后进行初始化;但 nameversion 没有依赖关系,编译器会根据二者的申明顺序确定初始化顺序。

我们先来探索对依赖关系的处理方式。总体思路如下:为赋值语句确定三种状态:NotStarted, Pending, Done. 对于 Pending 状态中的语句,编译器记录两方面的信息:

  1. 该语句所依赖的其它变量的数目,记为 order. order = 0 表示该语句不依赖任何其他变量

  2. 依赖于该赋值语句的其他赋值语句列表,记为 blocking map. 对于赋值语句 A, blocking[A] 表示所有依赖 A 的赋值语句构成的列表

例如如下代码:

AssignA 依赖变量 a, b, 所以 order[AssignA] = 2, 并且 blocking[AssignB] = [AssignA], blocking[AssignC] = [AssignA].

有了上述信息,处理步骤如下:

  1. 对于 NotStarted 的赋值语句,计算其 order 值与 blocking 列表,并将其标记为 Pending

  2. 对于所有 order=0 的 Pending 语句,将其放入一个 ready queue 中,标记为 Done 并将其 blocking 列表中的所有语句的 order 值减一

  3. 重复第二步直到处理完所有 Pending 语句

处理逻辑定义在函数 initOrder() 中,删掉错误检测相关的代码,核心逻辑如下:

其中 staticinit.Schedule 用来判定初始化时间,将会在下一节讨论。结构体 InitOrder 用来保存 order 及 blocking 列表的信息,定义如下:

函数的参数是 typecheck.Target.Decls, for 循环用来遍历所有 NotStarted 状态的赋值语句,并使用方法 processAssign() 来计算 order 与 blocking 属性,该方法代码如下:

flushReady() 处理所有 order=0 的语句并修改对应的 blocking 列表, flushReady() 会使用方法 s.StaticInit() 进行静态初始化,如果赋值语句必须在运行时进行,则将其保存在 s.Out 中,该属性是方法的返回值。方法的代码如下:

如果 flushReady() 在修改 blocking 列表有多个语句的 order 值都降为了0, 那么此时如何确定其顺序呢?该顺序通过最低优先级队列来实现,类型 declOrder 实现了 heap 接口:

这里的 Less 方法定义了两个语句的先后顺序:按照语句在代码中的位置进行排序,如果语句 A 在代码中排在 B 前面,那么就先对 A 进行初始化,再对 B 进行初始化。

关于堆的实现可以参考 container/heap 中的实现,这里不再展开。

初始化时间

对于已经加入 ready queue 中的赋值语句,编译器需要判断何时对其进行初始化,是需要推迟到运行时进行,还是在编译时便可完成。这个工作由定义在文件 cmd/compile/internal/staticinit/sched.go 中,入口函数是结构体 Schedule 的方法 StaticInit(), 该方法作为 flushReady() 的参数,在处理 ready queue 时使用。

如下代码反映了该部分的总体逻辑:

方法 tryStaticInit() 会尝试着对赋值语句进行静态初始化,如果返回值为 false, 则说明该语句必须在运行时执行,程序会将其加入到列表 Schedule.Out 中,如果回顾 initOrder() 函数的话,可以发现该属性便是最终的返回值。

那么哪些语句的初始化可以在编译时完成呢?总体来说,如果右侧表达式是字面量初始化或者简单的类型转换,那么那么赋值操作便可以在编译时进行。对右侧表达式进行判断的逻辑都在方法 Schedule.StaticAssign() 中,此处不再详细展开,我们可以通过下一节介绍的方法查看 Dump 信息或者调试程序。

最后更新于

这有帮助吗?