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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://gocompiler.shizhz.me/6.-golang-bian-yi-qi-chu-shi-hua-ren-wu/6.4-fu-zhi-yu-ju.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
