最后更新于
最后更新于
对于全局变量的赋值语句,编译器需要确定其初始化的顺序及初始化时间,我们分别加以讨论。
确定初始化顺序的逻辑在文件cmd/compile/internal/pkginit/initorder.go
中,入口函数是initOrder()
, 该函数在中的 Step 2 中被调用。
影响初始化顺序的因素有两个:一是依赖关系、二是申明顺序。例如如下代码:
其中 nameAlias
依赖于 name
, 所以即使它声明在 name
前面,其也应该在 name
之后进行初始化;但 name
与 version
没有依赖关系,编译器会根据二者的申明顺序确定初始化顺序。
我们先来探索对依赖关系的处理方式。总体思路如下:为赋值语句确定三种状态:NotStarted, Pending, Done. 对于 Pending 状态中的语句,编译器记录两方面的信息:
该语句所依赖的其它变量的数目,记为 order. order = 0 表示该语句不依赖任何其他变量
依赖于该赋值语句的其他赋值语句列表,记为 blocking map. 对于赋值语句 A, blocking[A] 表示所有依赖 A 的赋值语句构成的列表
例如如下代码:
AssignA 依赖变量 a, b, 所以 order[AssignA] = 2, 并且 blocking[AssignB] = [AssignA], blocking[AssignC] = [AssignA].
有了上述信息,处理步骤如下:
对于 NotStarted 的赋值语句,计算其 order 值与 blocking 列表,并将其标记为 Pending
对于所有 order=0 的 Pending 语句,将其放入一个 ready queue 中,标记为 Done 并将其 blocking 列表中的所有语句的 order 值减一
重复第二步直到处理完所有 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 信息或者调试程序。