对于全局变量的赋值语句,编译器需要确定其初始化的顺序及初始化时间,我们分别加以讨论。
初始化顺序
确定初始化顺序的逻辑在文件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
之后进行初始化;但 name
与 version
没有依赖关系,编译器会根据二者的申明顺序确定初始化顺序。
我们先来探索对依赖关系的处理方式。总体思路如下:为赋值语句确定三种状态:NotStarted, Pending, Done. 对于 Pending 状态中的语句,编译器记录两方面的信息:
该语句所依赖的其它变量的数目,记为 order. order = 0 表示该语句不依赖任何其他变量
依赖于该赋值语句的其他赋值语句列表,记为 blocking map. 对于赋值语句 A, blocking[A] 表示所有依赖 A 的赋值语句构成的列表
例如如下代码:
复制 var x = f (a, b, b) // 记为 AssignA
var a = g () // 记为 AssignB
var b = h () // 记为 AssignC
AssignA 依赖变量 a, b, 所以 order[AssignA] = 2, 并且 blocking[AssignB] = [AssignA], blocking[AssignC] = [AssignA].
有了上述信息,处理步骤如下:
对于 NotStarted 的赋值语句,计算其 order 值与 blocking 列表,并将其标记为 Pending
对于所有 order=0 的 Pending 语句,将其放入一个 ready queue 中,标记为 Done 并将其 blocking 列表中的所有语句的 order 值减一
处理逻辑定义在函数 initOrder()
中,删掉错误检测相关的代码,核心逻辑如下:
复制 func initOrder (l [] ir . Node ) [] ir . Node {
s := staticinit . Schedule {
Plans: make ( map [ ir . Node ] * staticinit . Plan ),
Temps: make ( map [ ir . Node ] * ir . Name ),
}
o := InitOrder {
blocking: make ( map [ ir . Node ][] ir . Node ),
order: make ( map [ ir . Node ] int ),
}
// Process all package-level assignment in declaration order.
for _, n := range l {
switch n. Op () {
case ir.OAS, ir.OAS2DOTTYPE, ir.OAS2FUNC, ir.OAS2MAPR, ir.OAS2RECV:
o. processAssign (n)
o. flushReady (s.StaticInit)
case ir.ODCLCONST, ir.ODCLFUNC, ir.ODCLTYPE:
// nop
default :
base. Fatalf ( "unexpected package-level statement: %v " , n)
}
}
return s.Out
}
其中 staticinit.Schedule
用来判定初始化时间,将会在下一节讨论。结构体 InitOrder
用来保存 order 及 blocking 列表的信息,定义如下:
复制 type InitOrder struct {
// blocking list
blocking map [ ir . Node ][] ir . Node
// ready queue
ready declOrder
order map [ ir . Node ] int
}
函数的参数是 typecheck.Target.Decls
, for
循环用来遍历所有 NotStarted 状态的赋值语句,并使用方法 processAssign()
来计算 order 与 blocking 属性,该方法代码如下:
复制 func (o * InitOrder ) processAssign (n ir . Node ) {
if _, ok := o.order[n]; ok {
base. Fatalf ( "unexpected state: %v , %v " , n, o.order[n])
}
o.order[n] = 0
// 通过 collectDeps() 函数解析依赖列表
for dep := range collectDeps (n, true ) {
defn := dep.Defn
// Skip dependencies on functions (PFUNC) and
// variables already initialized (InitDone).
if dep.Class != ir.PEXTERN || o.order[defn] == orderDone {
continue
}
// 设置 order 与 blocking 列表
o.order[n] ++
o.blocking[defn] = append (o.blocking[defn], n)
}
// 如果该节点没有依赖,则放入 ready queue 中
if o.order[n] == 0 {
heap. Push ( & o.ready, n)
}
}
flushReady()
处理所有 order=0 的语句并修改对应的 blocking 列表, flushReady()
会使用方法 s.StaticInit()
进行静态初始化,如果赋值语句必须在运行时进行,则将其保存在 s.Out
中,该属性是方法的返回值。方法的代码如下:
复制 func (o * InitOrder ) flushReady (initialize func ( ir . Node )) {
for o.ready. Len () != 0 {
n := heap. Pop ( & o.ready).( ir . Node )
if order, ok := o.order[n]; ! ok || order != 0 {
base. Fatalf ( "unexpected state: %v , %v , %v " , n, ok, order)
}
initialize (n) // 使用方法 StaticInit 进行处理
o.order[n] = orderDone
blocked := o.blocking[n]
delete (o.blocking, n)
// 修改 blocking 列表, 并将 order 降为 0 的语句推入 ready queue
for _, m := range blocked {
if o.order[m] -- ; o.order[m] == 0 {
heap. Push ( & o.ready, m)
}
}
}
}
如果 flushReady()
在修改 blocking 列表有多个语句的 order 值都降为了0, 那么此时如何确定其顺序呢?该顺序通过最低优先级队列来实现,类型 declOrder
实现了 heap
接口:
复制 type declOrder [] ir . Node
func (s declOrder ) Len () int { return len (s) }
func (s declOrder ) Less (i, j int ) bool {
return firstLHS (s[i]). Pos (). Before ( firstLHS (s[j]). Pos ())
}
func (s declOrder ) Swap (i, j int ) { s[i], s[j] = s[j], s[i] }
func (s * declOrder ) Push (x interface {}) { * s = append ( * s, x.( ir . Node )) }
func (s * declOrder ) Pop () interface {} {
n := ( * s)[ len ( * s) - 1 ]
* s = ( * s)[: len ( * s) - 1 ]
return n
}
这里的 Less
方法定义了两个语句的先后顺序:按照语句在代码中的位置进行排序,如果语句 A 在代码中排在 B 前面,那么就先对 A 进行初始化,再对 B 进行初始化。
关于堆的实现可以参考 container/heap
中的实现,这里不再展开。
初始化时间
对于已经加入 ready queue 中的赋值语句,编译器需要判断何时对其进行初始化,是需要推迟到运行时进行,还是在编译时便可完成。这个工作由定义在文件 cmd/compile/internal/staticinit/sched.go
中,入口函数是结构体 Schedule
的方法 StaticInit()
, 该方法作为 flushReady()
的参数,在处理 ready queue 时使用。
如下代码反映了该部分的总体逻辑:
复制 type Schedule struct {
// 保存需要运行时才能初始化的语句,已排序
Out [] ir . Node
Plans map [ ir . Node ] * Plan
Temps map [ ir . Node ] * ir . Name
}
func (s * Schedule ) append (n ir . Node ) {
s.Out = append (s.Out, n)
}
func (s * Schedule ) StaticInit (n ir . Node ) {
if ! s. tryStaticInit (n) {
if base.Flag.Percent != 0 {
ir. Dump ( "nonstatic" , n)
}
s. append (n)
}
}
方法 tryStaticInit()
会尝试着对赋值语句进行静态初始化,如果返回值为 false
, 则说明该语句必须在运行时执行,程序会将其加入到列表 Schedule.Out
中,如果回顾 initOrder()
函数的话,可以发现该属性便是最终的返回值。
那么哪些语句的初始化可以在编译时完成呢?总体来说,如果右侧表达式是字面量初始化或者简单的类型转换,那么那么赋值操作便可以在编译时进行。对右侧表达式进行判断的逻辑都在方法 Schedule.StaticAssign()
中,此处不再详细展开,我们可以通过下一节介绍的方法查看 Dump 信息或者调试程序。