> For the complete documentation index, see [llms.txt](https://gocompiler.shizhz.me/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gocompiler.shizhz.me/6.-golang-bian-yi-qi-chu-shi-hua-ren-wu/6.4-fu-zhi-yu-ju.md).

# 6.4 赋值语句

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

## 初始化顺序

确定初始化顺序的逻辑在文件`cmd/compile/internal/pkginit/initorder.go`中，入口函数是`initOrder()`, 该函数在[总体逻辑](/6.-golang-bian-yi-qi-chu-shi-hua-ren-wu/6.3-zong-ti-luo-ji.md)中的 Step 2 中被调用。

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

```go
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 状态中的语句，编译器记录两方面的信息：

1. 该语句所依赖的其它变量的数目，记为 order. order = 0 表示该语句不依赖任何其他变量
2. 依赖于该赋值语句的其他赋值语句列表，记为 blocking map. 对于赋值语句 A, blocking\[A] 表示所有依赖 A 的赋值语句构成的列表

例如如下代码：

```go
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].

有了上述信息，处理步骤如下：

1. 对于 NotStarted 的赋值语句，计算其 order 值与 blocking 列表，并将其标记为 Pending
2. 对于所有 order=0 的 Pending 语句，将其放入一个 ready queue 中，标记为 Done 并将其 blocking 列表中的所有语句的 order 值减一
3. 重复第二步直到处理完所有 Pending 语句

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

```go
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 列表的信息，定义如下：

```go
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 属性，该方法代码如下：

```go
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` 中，该属性是方法的返回值。方法的代码如下：

```go
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` 接口：

```go
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 时使用。

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

```go
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 信息或者调试程序。
