# 0. Golang 编译器代码浅析

![Let's GO!](/files/-Mb0JLJsB_kz-KLqRMP4)

#### 原创不易，谢谢支持!

![](/files/-Mb4H4VoE0iO_PxnrGkO)


# 1.1 编译器简介

CPU 是计算机执行指令的单元，每种架构的 CPU 都有自己的指令集，例如常见的 `x86` , `ARM` 等，由此构成的编程语言称为机器语言，由机器语言构成的程序是 CPU 唯一能够执行的程序，机器语言是面向机器的低级编程语言。

而在软件开发中，人的生产效率是第一需要考虑的因素，除此之外，我们还需要更多、更强的抽象能力与表达能力（例如并发、模块化、作用域等），来编写结构更加复杂的程序；像 `Java`, `Golang` 这类编程语言是面向人类的高级编程语言。

因此，从高级语言到机器语言的转换是执行任何程序的先行步骤，这个过程叫着编译，完成该过程的程序叫着编译器。编译是个复杂的过程，通常被划分为如下几个阶段：

词法分析 –> 语法分析 –> 语义分析 –> 中间代码生成 –> 机器无关代码优化 –> 目标代码生成 –> 目标代码优化

编译器是一个非常庞大复杂的系统，更准确地说，这其实是一个完整的领域。通常而言，程序语言设计者会采用已有的编译器工具链来构造不同的模块，例如使用 [Lex](https://en.wikipedia.org/wiki/Lex_\(software\)) 来构造词法分析器，使用 [Yacc](https://en.wikipedia.org/wiki/Yacc) 来构造语法分析器，然后配合自己实现的其他模块来组装成一个完整的编译器，这样的话技术栈将非常宽泛，深入学习该语言的编译过程就变成了一件非常困难的事情。通常情况下，编程语言的编译器都会使用另一种语言来实现，这对用户学习其编译过程也是极度不友好的。因此除非是编译器的实现者，对于大多数普通程序员而言，对编译器的知识通常只是停留在理论阶段，而无法与所使用的语言直接关联起来。对于自己所使用语言的编译过程知识的缺失，是造成无法深入了解、掌握甚至精通该语言的障碍之一。


# 1.2 Golang 编译器

Golang 在[1.5](https://golang.org/doc/go1.5)时实现了自举，即 Go 语言的整个编译器（Compiler）及运行环境（Runtime）都是用 Go 语言写成的，包括其词法分析器、语法分析器都是完整的 Go 语言程序。这给想要深入学习 Go 语言的用户提供了非常友好的支持，对于 Go 语言的任何特性，从语法规则到类型系统，我们都可以深入到编译器中去寻找实现原理，甚至可以自己动手给语言增加新的功能，例如箭头函数、内置函数等，即使无法在生产环境使用，这种实践也是非常有趣的。


# 1.3 Go 语言版本

该系列文章基于 [master](https://github.com/golang/go/tree/master) 分支的最新代码，截止本系列文章结束，最新的 commit 是 `d26fc68aa10dc8eda5ccdcc80d790e7df2fd9823`, 建议读者基于相同的 commit 来搭建项目及阅读该系列文章。

Go 编译器的开发工作一直在活跃地进行，从最初 C 语言的版本到实现自举，一直到当前阶段，Go Team 一边在增加新的语言特性，一边在对以往的模块进行重构，甚至重写。当前阶段最大的开发工作集中在泛型上，与此同时也在重写整个类型检查模块，在 1.16 版本发布之前，这部分的开发工作一直在 `dev.typeparams` 分支上进行，随着 1.16 的发布以及 1.17 开发工作的展开，该分支的所有内容已经合并到 `master` 分支（参考 [Planning Go 1.17](https://groups.google.com/g/golang-dev/c/DriIMM7VoLs)）。新版本的代码除了增加了对泛型的支持，整个类型检查系统与之前的版本有很大的差异，虽然目前并没有确定，但是根据最新的代码结构以及 [Russ’s Plan](https://groups.google.com/g/golang-dev/c/U7eW9i0cqmo/m/ffs0tyIYBAAJ), 从 1.18 版开始，随着泛型功能的发布，整个编译器应该大概率会采用新版本的类型检查系统（参见 [编译器开发组的讨论](https://github.com/golang/go/issues/43930)）。

本文开始写于 1.16 版本发布之前，最开始是基于 1.15.6 的代码，但随着对以上信息的了解，作者认为没有必要写一系列发表即过时的分析文章，尽管此时距离 1.18 的发布乐观估计还有一年时间，但考虑到代码结构上的巨大改变，同时为了覆盖正在开发的泛型特性，作者还是决定基于 `master` 分支的最新代码来进行分析，由于 `master` 分支的开发工作非常活跃，所以文中所引用的代码位置以及各种文件、函数等名称可能无法随时保持一致，请读者理解。等 Go Team 完成了该部分的开发工作之后，我将会选择一个 Frozen 分支并将所有引用的位置固定。


# 1.4 项目设置

要使用 `master` 分支的代码，就需要能够编译最新代码的环境，下列步骤是在 Linux 系统下配置项目的简易步骤（详细步骤参见[官方教程](https://golang.org/doc/install/source)）：

1. 下载 Go 语言编译器，使用[最新发布版本](https://golang.org/dl/)即可。解压后将文件夹更名为 `gobs` 并移动到某目录下，例如 `$HOME/Tools` 下。此时 `$HOME/Tools/gobs` 包含着最新的 Go 预编译发行版本
2. &#x20;设置编译工具链环境变量：

   ```
   export GOROOT_BOOTSTRAP=$HOME/Tools/gobs
   ```

   该环境变量用于搜索编译 Go 源码时的编译器工具链，因为只设置了该环境变量，所以该目录下的 go 只会在后续的编译步骤中被搜索使用到
3. &#x20;Clone 源代码并编译：

   ```
   git clone https://github.com/golang/go $HOME/Tools/go # Clone 源代码到目标目录
   cd $HOME/Tools/go/src
   git checkout d26fc68aa10dc8eda5ccdcc80d790e7df2fd9823 # checkout 与本文一致的代码
   ./all.bash                  # 编译源代码
   ```

   完成编译之后`$HOME/Tools/go` 下面会多出`bin` 与`pkg` 目录。`bin` 目录下面包含可执行文件 `go`, 而`pkg` 目录下包含着各个库编译后的对象文件（pkg/linux\_amd64）、工具链（pkg/tool）以及构建时的缓存文件（pkg/obj/go-build）。可以运行 `bin/go version` 查看当前版本：

   ```
       ./go version
       go version devel +ce19612a36 Thu Mar 4 15:10:16 2021 +0800 linux/amd64
   ```

   > 注意：工程中的很多代码是在构建时自动生成的，因此如果读者重新更新了代码，一定要先运行脚本 `./all.bash`, 否则 IDE 可能会提示编译错误，或者无法运行某些 UT
4. &#x20;设置环境变量：

   ```
       export GOROOT=$HOME/Tools/go
       export GOPATH=$HOME/go
       export PATH=$GOROOT/bin:$GOPATH/bin:$PATH
   ```

   注意这样设置之后整个系统都会使用 master 分支编译出来的 go, 如果你需要在本地编译生产环境的系统或者其他工具，则需要修改 `GOROOT`

编译器及整个工具链的代码在目 `$GOROOT/src/cmd/` 下面，该目录的结构也是一个典型的 go 语言工程，将该目录作为根目录引入 IDE 即可。

> 项目的配置方式不止一种，这里只是分享的基于作者自己开发环境（Archlinux）的个人配置，每个人偏爱的环境以及工具都不一样，只需要按照上述思路配置能够正常工作即可。


# 1.5 约定

这里约定在本系列文章中会经常使用到的一些词汇：

* GCROOT: 编译器代码根目录，即 `$GOROOT/src/cmd`


# 1.6 写作目的

该系列文章的主要目的是让读者在源码层面熟悉 Go 编译器的实现，因此会以代码讲解为主。但编译器的实现涉及到非常多的细节处理，本文会尽量避免大段的贴代码来对其进行分析（实际上也是不可行的），而是建议读者将文章当着一个参考，自己动手去源码中进行探索。因此每个主题的文章基本会按照如下结构进行组织：

1. 该模块的简介，需要涉及到的理论知识复习。例如词法、语法分析中涉及到很多形式语言与自动机的知识，文章都会先尽可能少地温习一下相关知识
2. 核心数据结构介绍，意在搭建该模块的骨架
3. 核心逻辑介绍，意在了解该模块实现的主体算法思路
4. 特殊案例分析，详细分析典型案例的处理流程

编译器前端和语言特性相关，包括词法分析、语法分析、类型检查等；而负责目标代码翻译的编译器后端，很多地方会涉及到具体的硬件体系知识，除非真的是这方面的从业人员，否则没有必要在这上面耗费太多的精力。因此我们将重点关注 Go 语言的编译器前端知识。

作者对编译领域的知识也只是一知半解，学习Golang编译器的目的只是为了提升自己对该语言的理解，以便写出更高质量的CRUD代码。本文也是在学习编译器代码时心血来潮，将笔记稍作整理最终促成几篇拙作，其中难免有疏漏、不当、甚至错误的地方，欢迎批评指正，共同学习进步。


# 2.1 简介

程序源代码是符合该语言文法的结构化文本，但对编译器而言，源代码只是一个很长的字符串，准确的说应该是一个字节流（byte stream），如何将程序的结构化信息从该字节流中提取出来，并构建出对应的数据结构，是编译器首先要做的事情。

从程序语言的层面来看，字节并不是构成程序的基本单元，当我们开始学习一门语言时，首先学习的是：

* 关键字： 用来声明程序的不同组件，例如函数、对象、控制语句、变量等
* 命名规则： 什么样的名字是合法的，什么样的是不合法的
* 基础数据类型： 数字、字符及字符串、布尔值、枚举类型等，以及数据类型的合法表达式，例如不同进制的数字如何表示，Unicode 字符如何表示，多行字符串如何表示等
* 操作符： 数字的加减乘除、指数运算等；字符串的串联；函数调用；位运算等

在编译器中，描述这些程序结构的基础组建的叫 Token, Token 中一般包含如下信息：

* 类型：标识该 Token 是一个数字，还是一个变量名，还是一个关键字
* 词素：从源码中识别出来的具体值，例如一个类型为数字的 Token 的词素可能是：3.1415
* 属性：例如该 Token 所在源代码中的哪行哪列

词法分析的目的就是将输入的字节流识别成一个个的 Token, 让原先的字节流变成 Token 流，如果 Token 的构成不符合规范，则报告错误信息。


# 2.2 代码结构

Go 编译器中与词法分析相关的代码位置如下：

* package 位置: $GCROOT/compile/internal/syntax
* source.go: 字符扫描&#x20;
* scanner.go: 词法分析器
* tokens.go: 各种类型的 token 声明
* tokens\_string.go: token 类型的 String 函数
* operator\_string.go: 操作符好的 String 函数


# 2.3 处理字符

Go 支持 UTF-8, 意味着可以使用任意 UTF-8 编码的字符为变量命名，而负责从连续字节流中处理字符的逻辑在`source.go`中，主要的结构体是：

```go
type source struct {
	in   io.Reader                        // 输入来源
	errh func(line, col uint, msg string) // 错误处理函数

	buf       []byte // 保存从 in 里面读取到的字节的缓存
	ioerr     error  // 如果从 in 读取内容到 buf 时发生 io 错误，则存放在该字段内
	b, r, e   int    // buf 中的三个标记位置，用来读取下一个字符，以及在 buf 中内容解析完了之后重新从 in 中读取新的内容到 buf 中
	line, col uint   // 用来标记当前字符的源代码位置
	ch        rune   // 最近读取到的 UTF-8 编码的字符
	chw       int    // ch 的字节长度，ASCII 中的字符长度为 1, 中文一个字符长度大于 1
}
```

其中重要的函数有两个：

```go
func (s *source) nextch() { /* ... */ }

func (s *source) fill() { /* ... */ }
```

`nextch`用来读取下一个字符，参见源代码可以发现其依赖`unicode/utf8`，以便从 buf 中读取下一个 UTF-8 编码的字符；`fill` 用来更新缓存 buf。任何时候 source 都保存着最新的字符读取状态，所以 nextch 不会返回字符，而是改变 source 的内部字段，获取当前字符直接访问属性`source.ch`就可以了。

我们通过 UT 来测试一下该模块的逻辑：在相同目录下创建`source_test.go`文件，并创建如下方法：

```go
func TestSource(t *testing.T) { 
    var s source 
    var buf bytes.Buffer
    buf.WriteString("abcdeABCDE 中文来了“ _")
    
    s.init(&buf, func(line, col uint, msg string) {
        fmt.Printf("Error: [msg: %s, line: %d, col: %d]\n", msg, line, col)
    })
    
    s.fill()
    
    for s.r != s.e {
        s.nextch()
    
        fmt.Printf("%v,", s.ch)
    }
    fmt.Println()
}
```

运行该方法，可以发现每个中文字符都得到了合理的解析：

> &#x20;`97,98,99,100,101,65,66,67,68,69,32,20013,25991,26469,20102,8220,32,95,`


# 2.4 扫描Token

识别 Token 的逻辑位于如下源代码中：

### tokens.go&#x20;

该文件内定义了与 Token 相关的三种类型：

```go
type token uint    // Token 类型
type LitKind uint8 // 如果当前 Token 是字面量，则标识该字面量类型
type Operator uint // 如果当前 Token 是操作符，则标识操作符类型
```

然后初始化了4种枚举常量，除了 token， LitKind, Operator 三种类型对应的常量外，还初始化了操作符优先级的常量，这些常量都在扫描 Token 时使用。详情请查看 tokens.go 文件，为了节约篇幅，这里不再贴出所有代码。

### scanner.go&#x20;

所有扫描 Token 的逻辑都通过结构 `scanner` 实现，其定义如下：

```go
type scanner struct {
    source
    mode   uint
    nlsemi bool // if set '\n' and EOF translate to ';'

    // current token, valid after calling next()
    line, col uint
    blank     bool // line is blank up to col
    tok       token
    lit       string   // valid if tok is _Name, _Literal, or _Semi ("semicolon", "newline", or "EOF"); may be malformed if bad is true
    bad       bool     // valid if tok is _Literal, true if a syntax error occurred, lit may be malformed
    kind      LitKind  // valid if tok is _Literal
    op        Operator // valid if tok is _Operator, _AssignOp, or _IncOp
    prec      int      // valid if tok is _Operator, _AssignOp, or _IncOp
}
```

&#x20;重要的字段包括：

* source: 用来扫描字符
* tok: 当前 Token 的类型
* lit: 保存当前 Token 的字面量
* kind: 标识当前 lit 的种类
* op: 记录当前操作符
* prec: 标识当前操作符的优先级

扫描 Token 的逻辑在`next` 中，该方法首先忽略掉空白字符，然后通过当前字符并根据其内容解析出下一个 Token. 我们来详细看一下其如何解析出程序中的标识符的。

标识符（Identifier）就是程序中用于命名的那些词素，可以抽象的理解为用来绑定一个值的“变量”（先不要将此处的变量与程序中通过 `var` 申明的变量混淆，此处的变量是更加抽象的概念），先通过一个程序片段来理解一下：

```go
package gostudy

import fmtalias "fmt"

func main() {
    content := "hello, gopher!"
    fmtalias.Println(content)
}
```

其中用于命名的词素包括：

* gostudy: 为包命名
* fmtalias: 为所引入的包 fmt 命名
* main: 为函数命名
* content: 为局部变量命名
* Println: 绑定包中的函数值

这些标识符有时也被直接称为符号（Symbol），其 Token 类型被定义为`_Name`. Golang 将关键字也归为此类，在 init 方法中对关键字对应的 Token 类型做了初始化：

```go
func hash(s []byte) uint {
    return (uint(s[0])<<4 ^ uint(s[1]) + uint(len(s))) & uint(len(keywordMap)-1)
}

var keywordMap [1 << 6]token // size must be power of two

func init() {
    // populate keywordMap
    for tok := _Break; tok <= _Var; tok++ {
        h := hash([]byte(tok.String()))
        if keywordMap[h] != 0 {
            panic("imperfect hash")
        }
        keywordMap[h] = tok
    }
}
```

init 将所有关键字的 Token 类型存放在数组`keywordMap` 中，后续逻辑依此来判定扫描的标识符是否是关键字。

Golang 规范的标识符命名规则是：

* 名字由数字、英文字母、下划线、或者编码超过两个字节的 Unicode 字符（编码数字大于 utf8.RuneSelf）组成
* 不能以数字开头
* 大小写敏感
* 不能使用关键字
* 长度不限制

函数`next` 在过滤了空白字符后，首先就会判断当前字符是否是一个标识符的开端，其逻辑如下：

```go
func (s *scanner) next() {
    // 忽略开头代码
    if isLetter(s.ch) || s.ch >= utf8.RuneSelf && s.atIdentChar(true) {
        s.nextch()
        s.ident()
        return
    }
    // 忽略后续代码
} 
```

如果当前字符是合法的标识符开端，则通过 `scanner.ident()` 方法扫描对应标识符并返回。该方法也比较紧凑，我们可以贴出来看看：

```go
func (s *scanner) ident() {
    // accelerate common case (7bit ASCII)
    for isLetter(s.ch) || isDecimal(s.ch) {
        s.nextch()
    }

    // general case
    if s.ch >= utf8.RuneSelf {
        for s.atIdentChar(false) {
            s.nextch()
        }
    }

    // possibly a keyword
    lit := s.segment()
    if len(lit) >= 2 {
        if tok := keywordMap[hash(lit)]; tok != 0 && tokStrFast(tok) == string(lit) {
            s.nlsemi = contains(1<<_Break|1<<_Continue|1<<_Fallthrough|1<<_Return, tok)
            s.tok = tok
            return
        }
    }

    s.nlsemi = true
    s.lit = string(lit)
    s.tok = _Name
}
```

该方法的逻辑与 Golang 中对标识符的规范完全一致，其一直读取合法的标识符字符，再通过 `s.segment()` 取出标识符词素；然后根据之前初始化的 `keywordMap` 判断其是否是语言内置的关键字，如果是关键字则设置 tok 为对应的关键字类型，否则便将 tok 设置为 `_Name` 类型，并将词素存放在 lit 属性中。

> &#x20;nlsemi: Golang 中使用分号来作为语句结束的标识符，但是大多数情况下都可以忽略掉，忽略的规则见：<https://golang.org/ref/spec#Semicolons>. 该字段用来判断如果当前 token 是当前行的最后一个 token 的话，是否可以自动插入一个分号。根据规则可见，如果该 token 是关键字 break, continue, fallthrough 与 return 之一时，可以插入分号；而当该 token 是一个标识符时，总是可以插入分号。

我们来跑一下测试代码看看 scanner 对标识符的解析情况。在 `scanner_test.go` 中新增如下测试代码：

```go
func TestScanIdentTokens(t *testing.T) {
    code := `package gostudy

    import fmtalias "fmt"

    func main() {
        content := "hello, gopher!"
        fmtalias.Println(content)
    }
`

    var scn scanner

    scn.init(strings.NewReader(code), func(line, col uint, msg string) {
        fmt.Printf("%d:%d: %s\n", line, col, msg)
    }, 0)

    scn.next()
    for scn.tok != _EOF {
        if scn.tok == _Name {
            fmt.Printf("Token Type: %s, lit: %s\n", tokStrFast(scn.tok), scn.lit)
        }

        // 关键字标识符不会设置 lit 字段
        if scn.tok >= _Break && scn.tok <= _Var {
            fmt.Printf("Token Type: %s\n", tokStrFast(scn.tok))
        }
        scn.next()
    }
}

```

运行后可以得到结果：

> ```
> Token Type: package
> Token Type: name, lit: gostudy
> Token Type: import
> Token Type: name, lit: fmtalias
> Token Type: func
> Token Type: name, lit: main
> Token Type: name, lit: content
> Token Type: name, lit: fmtalias
> Token Type: name, lit: Println
> Token Type: name, lit: content
> ```


# 2.5 总结

不管对哪个语言，词法分析的内在工作原理都是相似的：根据语言的词法规则将输入字节流转换为 Token 流。唯一不同的是各语言的词法规则，所以通常情况下会使用一个词法生成器来构建词法分析器，[Lex](https://en.wikipedia.org/wiki/Lex_\(software\)) 是流行的词法生成器。但 Golang 选择了自己实现，其核心 `scanner.go` 只有 1000 行不到的代码，这也为我们学习 golang 的词法解析提供了好机会。

&#x20;scanner 最重要的对外接口就是 `next` 方法，其职责非常简单：根据 golang 的词法规则解析下一个 Token. 其虽然不是严格意义上的 DFA(处理 `..` 时需要回滚一个字符), 但大体工作原理是一致的， scanner 是后续语法分析器的重要组成部分。


# 3A.1 语法分析简介

{% hint style="info" %}
本章将简略地介绍语法分析的理论知识，以便让读者对文法、CFG、LALR 等描述编译器语法分析的专业术语有清晰的认识。但本章知识对于学习 Go 的语法解析器并不是必须的，不感兴趣的读者可以直接跳过直接进入[Golang 编译器 - 语法分析](/3.-golang-bian-yi-qi-yu-fa-fen-xi/3.1-jian-jie)一章。
{% endhint %}

经过词法扫描，编译器获得了一个 Token 流，语法分析会解析这些 Token 所代表的程序结构信息，并根据程序语言的文法规则，将 Token 流构造成一颗抽象语法树（Abstract Syntax Tree），简称 AST。

AST 是编译阶段的重要数据结构，也是很多后续的操作的基础。


# 3A.2 文法

## 3.2.1 定义

自然语言有明确的语法，用来约束如何构建一个合法的句子。程序语言也一样，这个控制程序结构的规则叫着文法。文法是用来定义形式语言结构的规则，其规范的定义如下：

> 1. 一个终结符号集合, 例如：英文字母
> 2. 一个非终结符号集合，也被叫着文法变量
> 3. 产生式，符号的转换规则，通用形式为：α -> β，其中 α 与 β 可以是任意形式的符号串。例如 S ->（），表示任何 S 都可以被一个括号对替换。产生式的右边可以是终结符号与非终结符号的任意组合
> 4. 开始符号

概括地说，文法就是生成符号串的规则。

## 3.2.2 上下文无关文法 - Context-Free Grammar(CFG)

程序语言的文法通常使用一种叫着上下文无关文法的形式来表示。其对产生式做了如下限制：α -> β 中，α 只能是一个非终结符号。这也是“上下文无关”的意义：使用产生式将任意非终结符号 A 替换为其他符号串时，跟 A 所在的位置、以及 A 旁边的符号无关。

例如下面文法就是上下文无关文法，用来生成任意的合法括号对：

> S -> (S) | SS | ()
>
> 其中：&#x20;
>
> 1. 终结符号集合：由 "(" 与 ")" 组成&#x20;
> 2. 非中介符号集合：只有 S&#x20;
> 3. 产生式有三个&#x20;
> 4. 开始符号：S

对编译器而言，任何源程序都是一个字符串，程序语言的文法确定了程序的合法结构。只是程序文法中的终结符号不是单个字母，而是词法分析的输出结果：Token

## 3.2.3 推导 - Derivation

通过文法生成一个字符串的整个过程被称为一个推导。考虑上面生成括号对的文法：`S ->（S）-> （SS）->（（）S）->（（）（））`就是一个推导。

* 最左推导 \
  每次都使用最左边的非终结符号进行推倒的过程，例如下面就是一个最左推导：

  `S -> SS ->（）S ->（）（S）->（）（（））`
* 最右推导 \
  每次都使用最右边的非终结符号进行推倒的过程，例如下面就是一个最右推导：

  `S -> SS -> S（S）-> S（（））->（）（（））`

## `3.2.4 解析树 - Parse Tree`

解析树是一个推导过程的可视化，其叶子节点都是终结符号，中间节点都是非终结符号，叶子节点从左到右的排列就是被推导的终结符号串。如下例：

推导：`S -> SS -> S（S）-> S(())->()(())`解析树：

![Parse Tree](/files/-Mb0ZsCQY_-Fh4QtW4YM)

解析树只反映了整个字符串的推导方式，并没有体现其推导顺序。例如根据上面的解析树无法得知采用的是最左推导还是最右推导。


# 3A.3 语法解析

如果一个程序是良构的，那么代表该程序的 Token 序列就一定可以通过该程序的文法推导出来，并且只有一种推导方式，语法解析的核心逻辑就是还原该推导过程。如果将解析过程看着是构建一个解析树的过程，那么根据构建方式的不同，可以分为自顶向下与自底向上两种。接下来我们依次对其进行分析。


# 3A.3.1 自顶向下（Top-Down）

从解析树根节点开始，逐步向叶子节点构建整颗解析树的过程叫着自顶向下解析。自顶向下解析就是还原一个最左推导的过程。

本节我们先介绍递归下降算法，再介绍一种受限的自顶向下的解析算法 - 基于LL（1）文法的预测/匹配解析器。


# 3A.3.2 自顶向下 - 递归下降

递归下降解析（Recursive-Descent Parsing）为文法中每个非终结符号定义一个解析程序，整个解析过程就是递归调用各个非终结符号解析程序的过程，其程序结构与文法结构是同构的。

&#x20;我们来看一个递归下降的详细例子。考虑文法：

> S -> cAd\
> A -> ab\
> A -> a&#x20;

&#x20;假设有输入 w=cad ，递归下降的解析过程为：

> &#x20;下列步骤中符号 | 代表当前位置

**1. 解析树符号串当前位置：｜S**

输入流当前位置：｜cad

当前处在解析树的根节点，对应的开始符号 S 只有一个产生式，所以我们选择将其展开后得到解析树为：<br>

![Fig. Step 1](/files/-Mb0iG4VT3OCeC3gaJET)

对应的符号串为 cAd, 下一个字符为 c, 与输入流中下一个字符相匹配，所以我们将两边都前进一个符号。

**2. 解析树符号串当前位置：c|Ad**

输入流当前位置：c|ad

文法中符号 A 对应的产生式有两个，我们先选择 A -> ab 来尝试，得到解析树为：

![Fig. Step 2](/files/-Mb0ieR4qK4lfZyXW8hJ)

对应的符号串为 cabd, 下一个符号为 a, 与输入流中下一个字符匹配，于是我们将两边都前进一个符号

**3. 解析树符号串当前位置：ca|bd**

输入流当前位置：ca|d

解析树中下一个符号是终结符号 b，直接将其与输入符号相对比，而输入流中下一个符号是 d, 不相匹配。这说明我们之前所选择的产生式是不正确的，需要回溯到对应的位置重新选择。于是我们将两边都回到第 2 步中的起始位置

**4. 解析树符号串当前位置：c|Ad**

输入流当前位置：c|ad

这次选择产生式 A -> a, 得到解析树：

![Fig. Step 4](/files/-Mb0janc4u3w9ql_KYgp)

对应的符号串为 cad, 下一个符号为 a, 与输入流中下一个字符匹配，于是我们将两边都前进一个符号

**5. 解析树符号串当前位置：ca|d**

输入流当前位置：ca|d

解析树下一个符号是终结符号 d, 与输入流中下一个符号一致。将两边都前进一个符号后都到达了符号串的末尾，所以解析结束，第 4 步中的解析树即为最终结果

通过整个例子我们发现递归下降的解析方式可能需要回溯，因为当遇到有多个产生式的非终结符号时，我们的算法并没有提供合适的策略来明确应该选择哪一个，所以只有挨个尝试进行穷举。

递归下降算法还有个缺点是文法中不能包含左递归，通过上面例子我们很容易发现如果某个符号的产生式包含左递归的话，则会导致算法进入死循环。


# 3A.3.3 自顶向下 - LL(1)文法

预测/匹配 解析器（Predicate/Match Parser）

## 3.3.3.1 解析逻辑

对于解析器而言，其输入有两个：

* 文法 - G
* 待解析的字符串 - w, 解析器对其从左到右进行扫描

w 是一个确定的字符串，而解析器的目的就是找到从 G 中推导出 w 的方式。

因为文法是用来定义字符串的生成规则的，所以我们也可以将其看着是一个字符串生成器，该生成器从开始符号开始，随着解析器逻辑的进行，依据所选择的产生式来动态地生成后续的字符串，我们将该字符串叫着 g。解析过程就是让 G 生成的 g 和 w 一模一样。

随着解析器从左到右地同步扫描 g 和 w, 其内部工作逻辑如下：

1. 如果 g 中当前为终结字符，则将其与 w 中的当前字符进行对比，如果相同则两边都各前进一个字符；如果不相同则报错
2. 如果 g 中当前为非终结字符，则为其选择一个产生式对其进行展开，然后跳回步骤 1, 如果没有合适的产生式备选项，则报错
3. 如果 g 和 w 同时扫描完成，则完成推导
4. 如果 g 和 w 只有其中之一扫描完成，则说明无法从 G 推导出 w, 报告错误

递归下降需要回溯的原因是因为步骤 2 中选择产生式的方式是毫无根据的“猜”，因而只能一个一个地试。考虑任何中间时刻，解析器与两个输入的位置如下：

![](/files/-Mb0kxwtf-w5nJDe3dAB)

可以看见 w 的下一个字符是 m, g 的下一个字符是非终结符号 A, 所以如果 G 能够推导出 w 的话，那么不管经过多少步的推导，A 所产生的第一个终结字符也必须是 m.

我们可以根据这种思路来“预测”如何对 A 的产生式进行选择：对于文法 G ，我们可以提前建立起一个映射表：(A, a) -> ｛ A -> α ｝, 其中 A 是任意非终结字符，a 是任意终结字符，A -> α 最终推导出的第一个终结字符是 a.

如果该映射表右边的集合最多只有一个产生式的话，那么我们每次只需要在 w 中向前查看一个字符就可以唯 一确定如何选择，但如果该集合有多个产生式的话，则还需要继续向前查看多个字符来进一步确定。

这种通过提前查看 w 中字符的技术叫着 Look ahead, 简称 LA(k), 其中 k 代表向前查看的符号个数，在实际应用中通常只需要查看一个字符，所以是 LA(1).

但是如何构建文法 G 的预测表，是我们当前需要解决的问题。

## 3.3.3.2 构建预测表

我们来看对于一个文法 G, 如何构建预测表。首先我们定义两个函数：

* &#x20;FIRST(A) 通过 A 能够推导得到的第一个终结符号所组成的集合。即如果 a 在 First(A) 中，那么一定至少有一个推导长这样：A -> … -> aα

  &#x20;对于任意非终结字符 A，构建 First(A) 集合 I 的思路如下：

  1. 对于所有形如：A -> aα 的产生式，将 a 放入 I 中
  2. 如果存在 A -> ε, 则将 ε 也放入 I 中
  3. 对于所有形如：A -> Bβ 的产生式，将 First(B) 放入 I 中
* &#x20;Follow(A) 在推导过程中，能够跟在非终结符号 A 后面的终结符号组成的集合。即如果 b 在 Follow(B) 中，那么一定至少有一个推导是这样的：A -> … -> αΒbβ。如果某个符号后面什么都可以不用跟，那么我们用 $ 符号来表示。

  &#x20;在图 中，如果 First(A) 中包含 ε，并且 First(B) 中包含 m, 那么我们可以选择 A -> … -> ε 来对 A 进行推导，这样也能使解析器继续工作下去。这就是我们需要 Follow 集合的原因：如果 A 最终可以推导成一个空字符串，也就是说我们可以将其直接抹掉，那么能够合法出现在 A 后面的终结字符决定了解析器在解析过程中是否真的可以将其直接抹掉。再回到 这个例子中，如果 First(A) 包含 ε，但是 First(B) 中如果不包含 m 的话，我们就无法选择 A -> … -> ε 来抹掉 A, 否则解析就无法进行下去了。

  &#x20;对于文法 G, 为其任意非终结字符 B 构建 Follow(B) 集合的思路如下：

  1. 将 $ 放入 Follow(S) 中，S 为开始符号
  2. 对于所有形如：A -> αΒβ 的产生式，将 First(β) 所有字符放入 Follow(B) 中
  3. 如果存在：A -> αΒ，或者 A -> αΒβ 且 First(β) 包含 ε，那么将 Follow(A) 中所有字符放入 Follow(B) 中

有了文法 G 中所有非终结字符的 First 和 Follow 集合，我们就可以构建出预测表 T 了。针对 G 中的每个产生式 A -> α，处理逻辑如下：

1. 对于 First(A) 中的每个终结符号 a，将 A -> α 放入 T\[A, a] 中
2. 如果 First(A) 中包含 ε，则对于 Follow(A) 中每个终结符号 b, 将 A -> α 放入 T\[A, b] 中

可见预测表 T 是一个二维表格，其长相如下：

|   | a | b | c |
| - | - | - | - |
| A |   |   |   |
| B |   |   |   |
| C |   |   |   |

中间每个表格存放的是每行非终结符号的产生式。

## 3.3.3.3 例子

考虑如下文法：

> E -> E + T | T \
> T -> T \* F | F \
> F -> (E) | id

其定义了加法和乘法的表达式，并乘法符号 \* 的优先级高于加法符号 +。该文法是左递归的，我们先消除左递归，得到等价文法：

> E -> TE’ \
> E’ -> + TE’｜ε \
> T -> FT’ \
> T’ -> \* FT’｜ε \
> F -> (E) | id

构建 First 集合：

* First(F) = First(T) = First(E) = ｛（，id｝
* First(E’) = ｛+, ε｝
* First(T’) = ｛\*, ε｝

构建 Follow 集合：

* Follow(E) = Follow(E’) = ｛），$}
* Follow(T) = Follow(T’) = ｛+, ），$｝
* Follow(F) = ｛+, \*, ）， $}

有了这两个集合，我们可以构建出预测表如下：

|    | id       | +          | \*          | (        | )       | $       |
| -- | -------- | ---------- | ----------- | -------- | ------- | ------- |
| E  | E -> TE’ |            |             | E -> TE’ |         |         |
| E’ |          | E’ -> +TE’ |             |          | E’ -> ε | E’ -> ε |
| T  | T -> FT’ |            |             | T -> FT’ |         |         |
| T’ |          | T’ -> ε    | T’ -> \*FT’ |          | T’ -> ε | T’ -> ε |
| F  | F -> id  |            |             | F -> (E) |         |         |

可以看见该预测表中，一个非终结字符与一个终结字符的组合最多只有一个产生式可以选择，所以解析器在对输入进行解析时，任何时候最多只需要向前查看一个符号，就可以确定选择哪个产生式，这便是LA（1）的由来。

基于该预测表，为字符串 w: `id + id * id` 构建解析树的过程如下：

![](/files/-Mb0lkfn75DA3SpQOlJf)

![](/files/-Mb0lqx-4IADrjGI5EO1)

![](/files/-Mb0lu8f0q9GRn3H54aX)

![](/files/-Mb0lyUnppc2DWZ7kDKY)

![](/files/-Mb0m0xs8xswPDsjpQOZ)

![](/files/-Mb0m3nfObd9HuOAru4t)

## 3.3.3.4 总结

LL(1) 中，第一个 L 代表从左到右扫描输入符号串，第二个 L 代表产生最左推导, （1）代表 LA(1).

有上述分析可见，一个文法是 LL(1) 的必要条件是能够为其构建出一个预测分析表。回想在 中提到的两个输入字符串 g 与 w, 任意时刻解析器的逻辑如下：

* 匹配：如果 g 中当前符号是终结符号，则将其与 w 中当前进行匹配
* 预测：如果 g 中当前符号是非终结符号，则根据预测表即 LA(1) 对使用哪个产生式进行预测

所以一个 LL(1) 解析器也叫着 预测/匹配解析器。


# 3A.3.4 自底向上（Bottom-Up）

从解析树的叶子节点开始，逐步向上构建出整颗解析树的过程叫着自底向上解析。自底向上解析是一个最右推导的逆过程。

考虑如下文法 G：

> E -> E + T | T \
> T -> T \* F | F \
> F -> (E) | id

输入字符串 w: id + id \* id

&#x20;对 w 自底向上的解析为（为了便于显示，已扫描部分字体&#x662F;***加粗斜体***，另外使用 | 标记解析器当前位置）：

> 1. ***`id`*** |+ id \* id                 扫描 id
> 2. -> ***`F`*** |+ id \* id               规约 id 到 F
> 3. -> ***`T`*** |+ id \* id               规约 F 到 T
> 4. -> ***`E`*** |+ id \* id               规约 T 到 E
> 5. -> ***`E +`*** |id \* id              扫描 +
> 6. -> ***`E + id`*** |\* id            扫描 id
> 7. -> ***`E + F`*** |\* id              规约 id 到 F
> 8. -> ***`E + T`*** |\* id              规约 F 到 T
> 9. -> ***`E + T *`*** |id             扫描 \*
> 10. -> ***`E + T * id`***|    扫描 id
> 11. -> ***`E + T * F`***|             规约 id 到 F
> 12. -> ***`E + T`***|                     规约 T \* F 到 T
> 13. -> ***`E`***|                             规约 E + T 到 E, 解析结束

&#x20;该示例中体现了自底向上解析过程中的两个核心动作：

* &#x20;规约（Reduction）

  &#x20;解析器在从左向右扫描输入符号串的过程中，在合适的位置识别出一个产生式的右边部分，并将其替换为产生式的左边字符；第 11 步进行的操作就是规约
* &#x20;移入（Shift）

  &#x20;如果所扫描的部分还无法完成规约，则继续扫描并将新符号加入进去，这个过程叫着移入（Shift）；上例中彩色部分符号串就是已经扫描的部分，第 10 步所做的操作便是移入。

&#x20;不管是自顶向下还是自底向上，其面临的核心问题都是如何选择产生式：

* 自顶向下解析中，该问题体现在面临产生式左边的非终结符号时，如何选择合适的产生式右边；
* 而在自底向上解析中，是如何识别出产生式的右侧部分，然后将其规约成产生式的左侧符号。 例如上例第 11 步时，如何知道需要将 id 规约成 F, 然后进一步将 T \* F 规约成 T, 而不是直接将 T 规约成 E 呢？

&#x20;在扫描过程中，如何正确地识别出产生式的右侧部分并及时进行规约，是自底向上解析的核心问题，同自顶向下推导一样，我们也需要为自底向上推导构建一个预测表。


# 3A.3.5 自底向上 - LR(0)项集及SLR预测表

假如解析器中已经扫描和规约了的符号串为 g, 则 g\[i:] 总是文法中某个产生式右侧的一部分，否则就说明输入符号串不符合文法结构，解析无法正常完成。在上例中，g 就是每一步中的粗体部分，第 11 步时，g = E + T  \*  F，g\[2:] = T \* F 便刚好是产生式 T -> T \* F 的右侧，那么此时正好可以对其进行规约；而第 9 步时，g = E + T *\**, 此时 g\[2:] = T \* 是 T -> T \* F 的一部分，还无法对其进行规约。

g 表示的是当前对已扫描符号串的规约情况，也可以称之为当前规约状态，该状态与输入符号串中的当前符号共同决定了解析器的下一步该如何操作。那么该状态内包含了哪些信息、如何为这些信息建立起表达模型呢？&#x20;

## 3.3.5.1 规约状态

我们知道在解析过程中，总是使用 g\[i:] 来进行规约，其中 i >= 0，如果 g 的长度为 n, 那么此时有 n 个符号串需要考虑。我们先来分析上例中的两个解析步骤：&#x20;

1. 步骤 9, g = E + T \* :&#x20;

   1. i = 0: g\[0:] = E + T \*, 不匹配任何产生式右侧&#x20;
   2. i = 1: g\[1:] = + T \*, 不匹配任何产生式右侧&#x20;
   3. i = 2: g\[2:] = T *\**, 匹配产生式 T -> T \* F 右侧中的前两个符号&#x20;
   4. i = 3: g\[3:] = *\**, 不匹配任何产生式右侧

   此时只能够继续移入操作，因为只有这样才有可能基于 c 的情况拼出一个完整的规约符号串。 由此可知，我们需要一种方式来表示产生式的合法前缀，这样在任意时刻我们都可以知道已经看到了产生式的哪些部分。例如本例中我们知道我们已经看到了 T -> T \* F 中的 T \* . 当然本例中合法前缀只有一个，在其他情况下可能会有多个，我们暂且称之为产生式前缀集合。
2. 步骤 8, g = E + T：&#x20;

   1. i = 0: g\[0:] = E + T, 匹配产生式右侧：E -> E + T&#x20;
   2. i = 1: g\[1:] = + T, 不匹配任何产生式右侧&#x20;
   3. i = 2: g\[2:] = T, 匹配产生式右侧：E -> T

   此时合法前缀有两个：｛E + T, T｝；而解析器的操作可以有三种选择：&#x20;

   1. 按照 E -> E + T 进行规约，规约之后 g = E&#x20;
   2. 按照 E -> T 进行规约，规约之后 g = E + E&#x20;
   3. 继续扫描下一个符号并进行移入操作，移入之后 g = E + T \*

   如果此时已经没有了后续输入，那么 a 就是正确的步骤，这样就完成了整个解析过程；而在本例中，下一个符号是 \* *,* 此时正确的操作是执行移入操作，因为文法中 E 后面不可能跟符号 \*, 所以如果执行了规约，不管是按照 1 或者 2, 都无法继续对剩下的符号完成解析。<br>

   由此可知，在确定合法前缀集的情况下，我们还需要知道如何根据当前输入符号来执行正确的操作

要表示产生式前缀，我们可以给产生式引入一个位置信息，该信息用“·”来表示，例如 A -> BCD 可以产生的合法前缀如下：

> A -> ·BCD // 表示希望接下来读入一个从 BCD 推导出来的符号串 \
> A -> B·CD // 表示已经规约出了符号 B, 希望接下来读入一个从 CD 推导出来的符号串 \
> A -> BC·D \
> A -> BCD· // 表示此时已经可以按照该产生式进行规约了

我们将每个包含位置信息的产生式叫着一个项（Item），之前所说的前缀集合简称为项集。

根据以上分析可以知道：一个项集就代表一个规约状态，不同的状态通过文法符号来完成转移。例如 1 中的项集为：｛T -> T \* ·F｝，而 2 中的项集为：｛E -> E + T·，E -> T·｝

如果我们能够为文法构建出所有的项集，并建立起项集之间的转换关系，那么自底向上解析的预测表就可以建立起来了。

## 3.3.5.2 构建项集

到目前为止，我们使用项集来表示当前合法的产生式前缀集合，但是这样的集合是完备的吗？例如上面第 1 步中我们知道项集为 {T -> T \* ·F}, 但是该集合包含了此时所有可供选择的产生式前缀了吗？答案是否定的。T -> T \* ·F 表示的意义是我们期望接下来读入能够规约成 F 的符号串，那么所有能够从 F 推导出来的符号串的第一个符号，都应该与当前项集中的某个项相匹配，也就是说，对于任何 F -> α 所构成的项 C -> ·α，也应该在该项集中。显然，如果 α 中第一个符号是非终结符号的话，就可以继续应用该规则，最终得到一个完整的项集。

对于一个项集而言，通过该算法得到的最终项集叫着该项集的闭包（Closure），一个闭包项集是一个完备的项集，即当前解析器能够在文法中选择的所有可能的产生式，其对应的项都在该项集中。

给定一个初始化项集 I, 求 Closure(I) 的算法为：

1. 将 I 中每个项加入到 Closure(I) 中；
2. 如果 A -> α·Bβ 在 Closure(I) 中，对于所有产生式 B -> γ，如果 B -> ·γ 不在 Closure(I) 中，则将其加入其中。一直应用该规则检查 Closure(I) 的项，直到没有新的项加入进来为止。

我们已经可以根据已知项集 I 构建 Closure(I) 了，那么如何为文法构建所有的项集闭包呢？从开始符号切入是个自然的想法，但是开始符号可能有多个产生式，我们可以给文法添加一个产生式：S’ -> S, 其中 S 是原先的开始符号，S’ 为新的开始符号，这样形成的新文法叫着增广文法（Augmented Grammar），这样的话我们让 I0 = {S’ -> ·S}, 则 Closure(I0) 就是该文法的起始状态。

有了 Closure(I0), 如何构建整个文法项集闭包以及其之间的转换关系呢？我们为文法定一个函数 GOTO(I, X), 其中 I 是一个项集，X 是一个文法符号。如果 A -> α·Χβ 在 I 中，那么 GOTO(I, X) 表示 Closure({A -> αΧ·β}), 也就是说，GOTO 函数确定了文法中闭包项集之间的转换关系。

有了 Closure 与 GOTO 两个函数，我们就可以为文法构建完整的闭包项集的集合 ---- 项集族，并确定项集之间的转换关系了，其算法为：

1. 给定文法 G, 创建其增广文法 G’, 令 C 为 G’ 的项集族，初始化为空集
2. 令 I0 = ｛S’ -> ·S｝，将 Closure(I0) 加入 C 中
3. 对于 C 中的每个项集 I, 对于 G’ 中的每个文法符号 X, 如果 GOTO(I, X) 不在 C 中，则将其加入其中
4. 循环第 3 步，直到没有新的项集加入到 C 中为止

&#x20;考虑上述文法 G, 其增广文法 G’ 为：

> E’ -> E \
> E -> E + T | T \
> T -> T \* F | F \
> F -> (E) | id

下图是通过上述算法为 G’ 求得的 C：

![LR(0) 自动机](/files/-Mb0strg29YPmPXCkKB5)

上图展现的实际上是一个 LR(0) 自动机，图中一个项集就是一个状态。解析器不管在哪一个状态，都可以根据当前符号唯一确定下一步操作。我们使用栈来保存状态信息，来看一个解析例子：id + id:

| 状态      | 已规约符号串  | 输入符号串   | 动作                             |
| ------- | ------- | ------- | ------------------------------ |
| 0       | ·       | id + id | 移入：移入 id 并转移到状态 3              |
| 0 3     | id·     | + id    | 规约：按照 F -> id 规约，弹出栈顶状态        |
| 0       | ·F      | + id    | 状态转移到 5                        |
| 0 5     | F·      | + id    | 规约：按照 T -> F 规约，弹出栈顶状态         |
| 0       | ·T      | + id    | 状态转移到 2                        |
| 0 2     | T·      | + id    | 规约：按照 E -> T 规约，弹出栈顶状态         |
| 0       | ·E      | + id    | 状态转移到 1                        |
| 0 1     | E·      | + id    | 移入：移入 + 并转移到状态 6               |
| 0 1 6   | E +·    | id      | 移入：移入 id 并转移到状态 3              |
| 0 1 6 3 | E + id· |         | 规约：按照 F -> id 规约，弹出栈顶状态        |
| 0 1 6   | E + ·F  |         | 状态转移到 5                        |
| 0 1 6 5 | E + F·  |         | 规约：按照 T -> F 规约，弹出栈顶状态         |
| 0 1 6   | E + ·T  |         | 状态转移到 9                        |
| 0 1 6 9 | E + T·  |         | 规约：按照 E -> E + T 规约，弹出栈顶状态     |
| 0 1 6   | ·E      |         | 已经规约出文法 G 中的开始符号，并且输入已经完结，解析完成 |

&#x20;一个项集闭包代表着解析器的一个状态，有了文法的状态及其转换关系，我们就可以构建预测表了。

## 3.3.5.3 构造SLR预测表

通过上面的解析示例可以发现，解析器在任意状态下，其合法的操作有如下三个：

1. 移入，总是与终结符号相关，并根据移入的符号自动完成状态转换
2. 规约，无法进行移入操作，并且已规约的符号串顶部有与当前状态匹配的可规约项；规约后回到上一个状态
3. 状态转移，总是与非终结符号相关

如果解析器无法完成任何一个操作，则解析失败并报错。

至此预测表的两个维度我们已经清楚了：一个是状态；一个是文法符号。我们也可以将（状态，符号）这个组合所对应的操作分为两类：

1. Action \
   状态与终结符号的组合，其操作是移入或者规约
2. Goto \
   状态与非终结符号的组合，其操作是完成一次状态转移

给定文法 G, 构造预测表的算法为：

1. 求出增广文法 G’ 的项集族 C = ｛I0, I1, I2, … ，In｝，i 代表 Ii 所对应的状态
2. &#x20;对于任意项集 Ii, 执行如下操作构造预测表的 Action 部分：

   1. 如果 A -> α·aβ 在 Ii 中并且 GOTO(Ii, a) = Ij, 那么将 Action\[i, a] 的动作设置为移入
   2. 如果 A -> α· 在 Ii 中，那么对于 Follow(A) 中的所有符号 a，将 Action\[i, a] 的动作设置为规约

   &#x20;如果上述两步出现了冲突，那么说明该文法比较复杂，需要使用更复杂的技术来构造预测表，例如通过 Look ahead 输入流中的后续符号来进一步确定当前步骤的操作。
3. 对于任意项集 Ii, 执行如下操作构造预测表的 Goto 部分： 对于所有的非终结符号 A, 如果 GOTO(Ii, A) = Ij, 那么将 Goto\[i, A] 设置为“状态转换到 j”
4. 将预测表中第 2 步与第 3 步没有设置的条目设置为报错

根据该算法，我们来为文法 G 构造预测表：

> &#x20;对文法 G 的各个产生式进行编号：
>
> 1. E -> E + T
> 2. E -> T
> 3. T -> T \* F
> 4. T -> F
> 5. F -> (E)
> 6. F -> id
>
> &#x20;下表中的各个动作的编码如下：
>
> * si: 执行移入（Shift）操作，并转换到状态 i
> * ri: 按照第 i 个产生式进行规约
> * i: 转移到状态 i

&#x20;最终得到的预测表如下：

|    | id | +  | \* | (  | )   | $   | E | T | F  |
| -- | -- | -- | -- | -- | --- | --- | - | - | -- |
| 0  | s3 |    |    | s4 |     |     | 1 | 2 | 5  |
| 1  |    | s6 |    |    |     | acc |   |   |    |
| 2  |    | r2 | s7 |    | r2  | r2  |   |   |    |
| 3  |    | r6 | r6 |    | r6  | r6  |   |   |    |
| 4  | s3 |    |    | s4 |     |     | 8 | 2 | 5  |
| 5  |    | r4 | r4 |    | r4  | r4  |   |   |    |
| 6  | s3 |    |    | s4 |     |     |   | 9 | 5  |
| 7  | s3 |    |    | s4 |     |     |   |   | 10 |
| 8  |    | s6 |    |    | s11 |     |   |   |    |
| 9  |    | r1 | s7 |    | r1  | r1  |   |   |    |
| 10 |    | r3 | r3 |    | r3  | r3  |   |   |    |
| 11 |    | r5 | r5 |    | r5  | r5  |   |   |    |

至此我们已经构建出了自底向上的预测表，其项集与项集的转换都仅仅依赖于当前输入符号，我们将其叫着 LR(0) 项集，将该预测表称为 SLR(Simple LR) 预测表。

但这种预测表的解析能力还是很有限的，如果文法通过当前符号无法确定下一步操作，我们就需要更多的上下文信息，通常的手段是继续向前查看更多的符号来明确下一步操作，即 LR(k), 实践当中通常是 LR(1)。


# 3A.3.6 自底向上 - LR(1)、LALR

## 3.3.6.1 LR(1)项集定义

有的文法构建出来的 LR(0) 项集可能会存在移入、规约冲突，例如：

> &#x20;S -> L = R | R \
> L -> \* R | id \
> R -> L

我们为该文法的增广文法构建两个项集闭包：

> &#x20;I0: \
> S’ -> ·S \
> S -> ·L = R \
> S -> ·R \
> L -> ·\*R \
> L -> ·id \
> R -> ·L
>
> &#x20;I1: \
> S -> L· = R \
> R -> L·

可见在 I1 中，我们既可以通过项 R -> L· 进行规约，也可以根据项 S -> L· = R 进行移入操作。想要解决这种冲突，解析器就必须读取更多的上下文信息来辅助自己做决策：即向前查看（Look ahead）符号。如果输入的下一个符号是 =, 那么就继续移入，否则就进行规约。

为了让 Look ahead 的符号能够与项集中的项进行匹配，我们必须让项保存更多信息，也就是说我们必须让项更加“精细化”，Look ahead 将要查看多少个符号，就必须向项里面引入等量的额外分量，来形成新的映射关系。我们在这里仅讨论 LR(1), 所以还需要往项里面引入一个变量，用来表示终结符号。修改之后，之前的项 A -> α·β 变成了 \[A -> α·β, a], 终结符号 a 表示该项合法的后继符号，即 Look ahead 符号为 a 时，该项才可能是一个可以应用的项。该类项称为 LR(1) 项，其集合称为 LR(1) 项集。

进一步思考可以发现在项 \[A -> α·β, a] 中，如果 β 不为空，则符号 a 其实没有作用，因为此时只可能进行移入操作；只有对于形如 \[A -> α·, a] 的项，如果下一个输入符号是 a 时，才能进行规约。由此可见引入 Look ahead 符号其实本质上是在协助判断是否可以进行规约，例如上面例子中，我们需要通过下一个符号来判断是否可以通过 R -> L· 进行规约。

## 3.3.6.2 规范LR(1)语法分析表

为了正确地构建文法的 LR(1) 项集族，我们需要先对用到的两个算法稍作修改：

* &#x20;Closure(I), 使用下述三重循环为项集 I 构建闭包项集：

  > for (I 中每个项 \[A -> α·Bβ, a]) \
  > &#x20;     for (文法中的每个产生式 B -> γ) \
  > &#x20;           for (First(βa) 中的每个终结符号 b) \
  > &#x20;                 将 \[B -> ·γ, b] 加入到 I 中

  重复该循环直到没有新的项加入到 I 中为止
* Goto(I, X) 所表示的项集闭包的构建算法如下：
  1. 初始化集合 K
  2. 对于 I 中每个项 \[A -> α·Xβ, a], 将 \[A -> αX·β, a] 加入到集合 K
  3. Closure(K) 即为 Goto(I, X) 的项集闭包

有了 Closure 与 Goto 算法，我们就可以为文法构建 LR(1) 项集族了：

1. 给定文法 G, 创建其增广文法 G’, 令 C 为 G’ 的项集族，初始化为空集
2. 令 I0 = ｛\[S’ -> ·S, $]｝，将 Closure(I0) 加入 C 中
3. 对于 C 中的每个项集 I, 对于 G’ 中的每个文法符号 X, 如果 GOTO(I, X) 不在 C 中，则将其加入其中
4. 循环第 3 步，直到没有新的项集加入到 C 中为止

对比 LR(0) 的项集族算法，其实只有 I0 不一样。现在我们可以为给定文法 G 构造其 LR(1) 的语法分析表了，其算法如下：

1. 求出增广文法 G’ 的项集族 C = ｛I0, I1, I2, … ，In｝，i 代表 Ii 所对应的状态
2. &#x20;对于任意项集 Ii, 执行如下操作构造 Action 部分：

   1. 如果 \[A -> α·aβ, b] 在 Ii 中，并且 Goto(Ii, a) = Ij, 则将 Action\[i, a] 的动作设置为移入
   2. 如果 \[A -> α·, a] 在 Ii 中，则将 Action\[i, a] 的动作设置为规约
   3. 如果 \[S’ -> S·, $] 在 Ii 中，则将 Action\[i, $] 设置为“接受”

   &#x20;如果上述步骤产生了冲突，则该文法就不是 LR(1) 文法，我们无法为其构建一个 LR(1) 分析表
3. 对于任意项集 Ii, 执行如下操作构造 Goto 部分： 对于所有的非终结符号 A, 如果 GOTO(Ii, A) = Ij, 那么将 Goto\[i, A] 设置为“状态转换到 j”
4. 将语法分析表中第 2 步与第 3 步没有设置的条目设置为报错

通过该算法得到的语法分析表成为规范 LR(1) 语法分析表。整个算法的思路与 LR(0) 语法分析表的思想是一样的，区别在于为了匹配 Look ahead 符号，在项中引入了额外的分量使项集更加精细化，从而导致产生了更多的项集，这也正是使用 Look ahead 的目的：通过更多的上下文信息将 LR(0) 中有冲突的项集划分开来。

## 3.3.6.3 LALR语法分析表

规范LR(1) 语法分析表已经将如何利用 Look ahead 符号来辅助解析过程的思路表达得很充分了，但额外增加的状态数量导致了对空间的需求量显著增加，为了使其具备实践价值，我们必须想办法减少状态数量。

我们在构建规范LR(1) 语法分析表时，每一个状态都是有用的，所以我们不可能直接扔掉一些状态来减少分析表的状态数量，那么剩下的策略就只能是合并：在不产生冲突的情况下，将多个状态合并为一个。

如何确定可以合并哪些状态呢？

我们知道对于同一个文法，LR(1) 项集比 LR(0) 项集多的原因是前者引入了额外的分量：Look ahead 符号。再回顾一下 LR(1) 项的形式：\[A -> α·β, a], 其中第一个分量是 LR(0) 项：A -> α·β，第二个分量是 LA 符号：a. 我们将 LR(1) 项集中仅由第一个分量组成的集合叫着 LR(1) 项集的核心（Core）。容易得出：任何一个 LR(1) 项集的核心，都有一个 LR(0) 项集与之对应。根据鸽巢原理：必定存在多个 LR(1) 项集的核心，与同一个 LR(0) 项集对应。

> 为什么 LR(1) 项集核心必定与一个 LR(0) 对应呢？
>
> 我们知道 LR(0) 项集所代表的意义是：当前情况下，所有可能的合法产生式集合，以及每个产生式当前所处的位置！这个意义不会因为我们向前查看了输入符号而发生改变，也就是说，在自底向上解析的过程中，在任意时刻，解析器当前状态所对应的那个项集，不管 LR(k) 中的 k 是多少，其项集核心都是一样的；LR(1) 多出来的项集是 LR(0) 项集中各个项与 Look ahead 符号组合形成的新状态，所以将 Look ahead 符号剔除之后，其必定与一个 LR(0) 项集对应。

唯一合理的假设是我们将核心相同的 LR(1) 项集合并成一个状态，但要注意一个问题是：合并之后的状态不能存在冲突。由此可以得到如下合并思路：对于任意 n 个核心相同的 LR(1) 项集，只要他们的移入、规约操作没有冲突，那么就可以将他们合并为一个 LR(1) 项集。通过该方式构建的语法分析表叫着 LALR 语法分析表，由于 LALR 分析表即利用了 LA 技术，又显著减少了状态数量，所以具备很好的实践价值。


# 3A.4 语法分析工具

上文中讨论了如何对文法进行语法分析，并详细讨论了如何构造 LL 及 LR 文法的语法分析表（即上文中提到的预测表），但这只是为了深入理解语法分析的原理，在实践中的大多数情况下，我们都会直接使用语法分析器生成工具来直接生成语法分析器。因为在上述分析中我们可以发现一个事实：语法分析的总体流程是固定的，不同的仅仅是不同文法的语法分析表。而语法分析表的生成算法也是确定的，所以只要能够将文法的表示结构化，就可以将其作为输入，通过语法分析器生成工具直接生成一个语法分析器出来。Yacc(Yet Another Compiler-Compiler) 就是这样的一个工具。


# 3A.5 总结

本文只是对语法分析的一个粗略分析，仅仅抽取了语法分析“理论性”的部分加以说明；实际的语法分析还有很多地方需要考虑，例如：

* 文法二义性处理
* 错误处理及恢复
* 语法分析表的压缩
* 性能

但这些方面都是“实践性”的，除了真的要尝试写一个编译器，否则即使不了解也不影响我们搭建该部分的知识体系。了解了这些知识，我们就可以来看看 Go 语言的语法分析逻辑了。


# 3B.1 简介

Go 采用了递归下降的解析方式进行语法分析，并构建出抽象语法树（Abstract Syntax Tree, AST），解析器以文件为单位进行分析，如果该文件内没有语法错误，则解析器会生成一颗 AST, 该 AST 是后续类型检查的基础。


# 3B.2 代码结构

主体解析逻辑涉及到的代码位置

* package 位置: $GCROOT/compile/internal/syntax
* parser.go: 语法解析器主题逻辑
* nodes.go: AST 数据结构声明
* syntax.go: 解析入口函数所在文件，入口函数为 `Parse()`
* pos.go: 跟踪源文件位置信息的结构体声明

辅助代码

* dumper.go: 可视化 AST
* printer.go: 以特定格式打印 AST

从编译器主函数进入语法分析的代码位置如下：

```go
// file: cmd/compile/internal/gc/main.go

func Main(archInit func(*ssagen.ArchInfo)) {
    // 忽略前置的编译器初始化代码
    
    // Parse and typecheck input.
    noder.LoadPackage(flag.Args())
    
    // 忽略后续代码
}

```

noder.LoadPackage() 在文件 `$GCROOT/compile/internal/noder/noder.go` 中, 该函数会调用语法分析的入口函数 `syntax.Parse()`


# 3B.3 数据结构

语法树相关的数据结构都在文件 `$GCROOT/compile/internal/syntax/nodes.go` 中，每个源文件对应一个如下结构体的实例：&#x20;

```go
// package PkgName; DeclList[0], DeclList[1], ...
type File struct {
    Pragma   Pragma // 编译指令，嵌入在源代码的注释中，形如：go://xxxx, 可以忽略不予考虑
    PkgName  *Name  // 该文件的包名
    DeclList []Decl // 该文件中包含的顶级声明
    EOF      Pos
    node     // 内嵌结构体，使 File 实现 nodes.go 中定义的 Node 接口
}
```

该结构是`syntax.Parse()`函数的返回结果，代表着一个源文件的 AST。其中重要的是 `DeclList`, 其包含了该源文件中所有的声明，声明（Declaration）是 Go 源文件顶级作用域中的唯一合法结构，声明在其内部递归地包含其他结构体。

该文件中定义的各种数据结构与 [Go 语言规范](https://golang.org/ref/spec) 中所定义的语言结构是一一对应的，在源代码中，很多数据结构的声明上面都有对应的文法。所有的数据结构可以归为如下几类：

### 3.3.1 声明（Declaration） <a href="#org28a536d" id="org28a536d"></a>

声明就是在程序里面说“我要创建一个 X, 他的名字是 Y”，其包含两件事情：一个是命名，一个是绑定。例如 Go 中的 `var name string` 就是一个变量声明，其在程序中取了一个名字：name, 然后将其与 string 这种类型绑定起来，翻译成前边的语言就是：我要创建一个字符串，他的名字是 name。注意 `name := "Golang"` 不仅包含了声明，而且包含了赋值语句（Statement），只是声明中的类型信息是隐式的。

[Go Spec](https://golang.org/ref/spec#Declarations_and_scope) 中对声明的阐述为：

> &#x20;A declaration binds a non-blank identifier to a constant, type, variable, function, label, or package. Every identifier in a program must be declared. No identifier may be declared twice in the same block, and no identifier may be declared in both the file and package block.

由此可见，声明其实就是创建程序组件并为其命名的操作，有了这些程序组件作为积木，我们才能搭建出整个程序大厦。在 Go 语言源文件中，合法的顶级声明包括：常量、类型、变量、函数、方法 以及 package, 下面都是合法的声明：

```go
type A struct{}    // 声明一个结构体类型，其名字是 A
type B interface{} // 声明一个接口类型，其名字是 B

const a = 1 // 声明一个常量，将其命名为 a，并将其初始化为 1. 因为常量值无法修改，所以常量的声明必须与初始化在一条语句内完成。

var b string // 声明一个变量，将其命名为 b

func sum(i, j int) int // 声明一个函数，将其命名为 sum, 没有函数体。可以通过编译指令 `go:linkname` 将其与其他地方的函数链接起来
func sub(i, j int) int { // 声明一个函数，将其命名为 sub, 并包含函数体
    return i - j
}

type Str string        // 声明一个类型 Str, 其 underlying type 是 string
type StrAlias = string // 声明一个类型别名 StrAlias，其与 string 类型等价
```

在 nodes.go 中，每一种声明都有对应的数据结构，作为示例，我们在这里仅仅看一下函数声明的结构：

```go
// func          Name Type { Body }
// func          Name Type
// func Receiver Name Type { Body }
// func Receiver Name Type
type FuncDecl struct {
    Pragma     Pragma
    Recv       *Field     // 如果该函数是一个方法，则表示 Receiver, 否则为 nil
    Name       *Name      // 函数名，是一个表达式（Expr）
    TParamList []*Field   // 类型参数，即泛型
    Type       *FuncType  // 函数类型，是一个表达式（Expr），其中包含函数的参数与返回值的列表
    Body       *BlockStmt // 函数体的语句（Statement），如果没有函数体则为空
    decl                  // 内嵌结构体，使该结构体实现接口 Decl
}
```

可以发现该结构体就是对文法的一种直接映射，其每个属性也代表了一个函数或者方法的某个代码块。

结构体 FuncDecl 可以表示函数、方法两类声明，实际上 Go 中函数与方法没有本质区别，方法与函数的区别在于方法的第一个参数默认绑定到调用的实例上，这与 Java 的 `this` 关键字是一样的。因为 Go 中函数是一类公民（First Class Citizen），所以我们甚至可以将方法赋值给变量，如下代码体现了方法与函数的等价性质：

```go
package main

import "fmt"

type people struct {
    name string
}

func (this people) greeting(friend string) {
    fmt.Println("My name is ", this.name, ", How are you ", friend)
}

func main() {
    f := people.greeting
    p := people{"Golang"}

    f(p, "Java") // 与 f.greeting("Java") 效果一样
}
```

### 3.3.2 表达式（Expression） <a href="#org062dc7b" id="org062dc7b"></a>

表达式代表的是可以对其进行求值运算的代码块，例如算术运算、函数调用、访问数组或者哈希表等。

[Go Spec](https://golang.org/ref/spec#Expressions) 中对表达式的阐述为：

> &#x20;An expression specifies the computation of a value by applying operators and functions to operands.

也就是说表达式代表的是一个计算，该计算过程通过将某种运算符（operator）应用到操作数（operand）、或者将函数应用到其参数上来完成。常见的运算符包括算术运算符、逻辑运算符、位运算符等，我们对使用这类运算符做“计算”的认知是很自然的，例如表达式 `i + j`, 运算符（Operator）是 `+`, 操作数（Operand）是 `i` 与 `j`. 但对于编译器而言，运算符的范围更加宽泛，例如赋值语句的右边只能是一个表达式，形如 `a := b` 中的 `b` 必须有值返回才能将其赋给 `a`. 例如如下代码：

```go
language := struct {
    Name string
    Age  int
}{
    Name: "Golang",
    Age:  100,
}
```

赋值语句右侧以关键字 `struct` 开始，所以对于编译器而言 `struct` 就是一个运算符，其操作数就是紧跟着的 `{}` 所包围的代码块，编译器根据该运算符“计算”出了一个类型值，然后为该类型值初始化了一个实例赋值给 `language`.

nodes.go 中定义的表达式类型比较多，包括如下种类：

1. &#x20;字面量 \
   所有的字面量都通过一个表达式来表示：

   ```
   type BasicLit struct {
       Value string  // 字面量
       Kind  LitKind // 字面量类型，包括整数、浮点数、字符串等
       Bad   bool    // true means the literal Value has syntax errors
       expr
   }
   ```
2. &#x20;数据访问 \
   例如访问数组元素、访问 map、访问 struct 的字段等。形如 t\[i] 通过下标访问数据的表达式定义如下：

   ```
   // X[Index]
   type IndexExpr struct {
       X     Expr // 被访问的对象，通过一个表达式来表示
       Index Expr // 下标，也通过一个表达式来表示
       expr
   }
   ```
3. &#x20;类型定义 \
   所有的类型定义都是表达式，这里我们仅仅看一下 struct 类型的表达式结构：

   ```
   // struct { FieldList[0] TagList[0]; FieldList[1] TagList[1]; ... }
   type StructType struct {
       FieldList []*Field    // 该 struct 内部所有字段的列表
       TagList   []*BasicLit // 每个字段的 tag, 通过下标与字段对应
       expr
   }
   ```

   &#x20;类型表达式在类型检查时会用来构建具体的类型对象

这里只是简单列举了几类表达式，所有的表达式定义详见源文件。

### 3.3.3 语句（Statement） <a href="#orga19437e" id="orga19437e"></a>

如果说表达式是程序的计算单元的话，那么语句就是程序的控制单元。

[Go Spec](https://golang.org/ref/spec#Expressions) 中对语句的阐述为：

> &#x20;Statements control execution.

例如 for 控制循环，if…else… 控制分支选项等。我们看一下 if 语句的结构：

```go
type IfStmt struct {
    Init SimpleStmt // if 语句的初始化语句，本身是一个语句
    Cond Expr       // 判断条件，一个返回值为 bool 的表达式
    Then *BlockStmt // 条件为 true 时执行的语句块
    Else Stmt       // 条件为假时执行的语句块，可以是 nil, *IfStmt, 或者 *BlockStmt 之一
    stmt
}
```

其他语句对应的结构体定义详见源文件。

### 3.3.4 其他 <a href="#org2ac81ee" id="org2ac81ee"></a>

除了表示程序块的数据结构，该文件内还定义了一些辅助结构，包括：

* 针对不同类型的几个接口，例如 `Node`, `Decl`, `Expr`, `Stmt` 等
* &#x20;Group

  ```
  // All declarations belonging to the same group point to the same Group node.
  type Group struct {
      _ int // not empty so we are guaranteed different Group instances
  }
  ```

  通过括号 `()` 括起来的一组声明都指向同一个 Group 实例。例如如下代码中所有的常量声明都会指向一个 Group 实例：

  ```
  const (
      name   = "Golang"
      friend = "Java"
  )
  ```


# 3B.4 构造语法树

### 3.4.1 解析入口 <a href="#org67f0cbe" id="org67f0cbe"></a>

当调用 `go build a.go b.go` 这个命令时，go 最终会调用对应平台的编译器来对源文件进行编译，所传入的参数 `a.go b.go` 也会最为参数传递给编译器，并最终在编译主函数中作为参数传给语法分析的入口函数 `LoadPackage` ，该函数会完成语法解析与类型检查，我们在这里只关心语法解析的部分。去掉不重要的部分，其主要代码为：

```go
func LoadPackage(filenames []string) {
    mode := syntax.CheckBranches
    if base.Flag.G != 0 {
        mode |= syntax.AllowGenerics // 如果给编译器传递了 -G 这个参数，则可接受泛型语法。通过 go build -gcflags "-G=3" 来传递
    }

    // Limit the number of simultaneously open files.
    sem := make(chan struct{}, runtime.GOMAXPROCS(0)+10) // 控制解析的并发数量

    noders := make([]*noder, len(filenames))
    for i, filename := range filenames {
        p := noder{
            err:         make(chan syntax.Error),
            trackScopes: base.Flag.Dwarf,
        }
        noders[i] = &p

        filename := filename
        go func() {
            sem <- struct{}{}
            defer func() { <-sem }()
            defer close(p.err) // 关闭 error channel
            fbase := syntax.NewFileBase(filename)

            f, err := os.Open(filename)
            if err != nil {
                p.error(syntax.Error{Msg: err.Error()})
                return
            }
            defer f.Close()

            p.file, _ = syntax.Parse(fbase, f, p.error, p.pragma, mode) // 语法解析入口
        }()
    }

    var lines uint
    for _, p := range noders {
        for e := range p.err { // 错误检查，p.err 是 channel, 只有当该文件解析完成后才会关闭，所以这里也实现了对上面异步、并发解析文件的同步
            p.errorAt(e.Pos, "%s", e.Msg)
        }
        if p.file == nil {
            base.ErrorExit()
        }
        lines += p.file.EOF.Line()
    }
    base.Timer.AddEvent(int64(lines), "lines")

    // 忽略后续类型检查的代码
}
```

该函数是语法分析的主函数，在第一个 for 循环中并发地解析各个源文件，代码 `p.file, _ = syntax.Parse(base, f, p.error, p.pragma, syntax.CheckBranches)` 便是解析逻辑的入口， `syntax.Parse` 的返回结果便是对应源文件的语法树。

### 3.4.2 初始化 <a href="#org700a88c" id="org700a88c"></a>

在 parser.go 中，解析器 `parser` 的定义如下：

```go
type parser struct {
    file    *PosBase
    errh    ErrorHandler // 错误处理函数
    mode    Mode
    pragh   PragmaHandler // 负责从注释中提取编译指令
    scanner               // 词法解析器

    base   *PosBase // current position base
    first  error    // first error encountered
    errcnt int      // number of errors encountered
    pragma Pragma   // pragmas

    fnest  int    // function nesting level (for error handling)
    xnest  int    // expression nesting level (for complit ambiguity resolution)
    indent []byte // tracing support
}
```

我们无需关心每个字段的意义，最重要的是负责词法解析的 `scanner` 是 `parser` 的内嵌字段，Go 的词法解析是在语法分析的驱动下完成的，所以词法分析器也是语法解析器的一部分。语法解析器在构建语法树的过程中需要记录每个程序结构的位置信息，以便生成错误信息或者调试信息，所以也有对应的字段。

`parser` 的初始化在文件 syntax.go 中，其代码如下：

```go
func Parse(base *PosBase, src io.Reader, errh ErrorHandler, pragh PragmaHandler, mode Mode) (_ *File, first error) {
    defer func() {
        if p := recover(); p != nil {
            if err, ok := p.(Error); ok {
                first = err
                return
            }
            panic(p)
        }
    }()

    var p parser
    p.init(base, src, errh, pragh, mode)
    p.next()
    return p.fileOrNil(), p.first
}
```

忽略其中的 `defer` 函数，该代码通过 parser 的 init 完成初始化，然后通过函数 `fileOrNil` 完成语法解析。

### 3.4.3 解析过程 <a href="#orgc5a2ba2" id="orgc5a2ba2"></a>

文件 parser.go 有两千多行代码，我们不需要逐行分析，甚至不需要浏览遍历每个函数，只需要对所有函数大致分类，再梳理一下核心逻辑即可。后续我们会通过一些典型的场景来对解析器进行测试。

parser 的方法主要可以分为如下几类：

1. 词法处理的辅助方法
   1. want: 判断下一个 Token 是否是指定的 Token
   2. advance: 从当前位置开始，跳过指定 Token 之前的所有 Token
   3. &#x20;list: 解析通过指定分隔符分隔开来的多个相同类型的结构，所以该方法名叫 `list`, 其实就是解析出某个结构体的 `list`, 这个 `list` 通常被括号或者花括号括起来。例如方法 `structType`:

      ```
      // StructType = "struct" "{" { FieldDecl ";" } "}" .
      func (p *parser) structType() *StructType {
          if trace {
              defer p.trace("structType")()
          }

          typ := new(StructType)
          typ.pos = p.pos()

          p.want(_Struct)
          p.want(_Lbrace)
          p.list(_Semi, _Rbrace, func() bool {
              p.fieldDecl(typ) // 解析 struct 中的每一个 field 声明
              return false
          })

          return typ
      }
      ```

      其中调用 `list` 方法就是依次解析 `struct` 中的每一个字段. struct 使用花括号来界定代码块，所以结束符号是 `_Rbrace`. Go 语言在行末会自动插入一个分号，这在词法分析时讨论过，所以此处的分隔符是 `_Semi`, 可知如果想要在一行定义多个字段，就需要使用分号进行分隔。
2. 程序结构的解析方法 在上文“数据结构”中讨论过的每个结构，几乎都有与之对应的解析方法，这些方法是真正的解析逻辑。
3. 错误处理 `errorAt`, `syntaxErrorAt` 等，其会详细描述错误原因、错误位置等信息，并最终调用 `parser` 中的错误处理函数进行处理。

解析的主控方法是 `fileOrNil`, 其代码如下：

```go
// SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .
func (p *parser) fileOrNil() *File {
    if trace {
        defer p.trace("file")()
    }

    f := new(File)
    f.pos = p.pos()

    // PackageClause
    if !p.got(_Package) {
        p.syntaxError("package statement must be first")
        return nil
    }
    f.Pragma = p.takePragma()
    f.PkgName = p.name()
    p.want(_Semi)

    // don't bother continuing if package clause has errors
    if p.first != nil {
        return nil
    }

    // { ImportDecl ";" }
    for p.got(_Import) {
        f.DeclList = p.appendGroup(f.DeclList, p.importDecl)
        p.want(_Semi)
    }

    // { TopLevelDecl ";" }
    for p.tok != _EOF {
        switch p.tok {
        case _Const:
            p.next()
            f.DeclList = p.appendGroup(f.DeclList, p.constDecl)

        case _Type:
            p.next()
            f.DeclList = p.appendGroup(f.DeclList, p.typeDecl)

        case _Var:
            p.next()
            f.DeclList = p.appendGroup(f.DeclList, p.varDecl)

        case _Func:
            p.next()
            if d := p.funcDeclOrNil(); d != nil {
                f.DeclList = append(f.DeclList, d)
            }

        default:
            if p.tok == _Lbrace && len(f.DeclList) > 0 && isEmptyFuncDecl(f.DeclList[len(f.DeclList)-1]) {
                // opening { of function declaration on next line
                p.syntaxError("unexpected semicolon or newline before {")
            } else {
                p.syntaxError("non-declaration statement outside function body")
            }
            p.advance(_Const, _Type, _Var, _Func)
            continue
        }

        // Reset p.pragma BEFORE advancing to the next token (consuming ';')
        // since comments before may set pragmas for the next function decl.
        p.clearPragma()

        if p.tok != _EOF && !p.got(_Semi) {
            p.syntaxError("after top level declaration")
            p.advance(_Const, _Type, _Var, _Func)
        }
    }
    // p.tok == _EOF

    p.clearPragma()
    f.EOF = p.pos()

    return f
}
```

该方法的解析逻辑分为三部分：

1. 解析 `package`
2. 通过一个 for 循环解析 `import` 声明
3. 通过 for 循环内部解析顶级声明，包括常量、类型、变量、函数，每次循环通过当前 Token 来进行区分具体类型。 `appendGroup` 函数会对声明做分组处理，即被 `()` 括起来的一组声明会指向同一个 `Group` 实例。

每个解析方法内部都会根据当前 Token 来判断接下来应该解析何种结构，我们来看一下如何解析函数声明的：

```go
// FunctionDecl = "func" FunctionName [ TypeParams ] ( Function | Signature ) .
// FunctionName = identifier .
// Function     = Signature FunctionBody .
// MethodDecl   = "func" Receiver MethodName ( Function | Signature ) .
// Receiver     = Parameters .
func (p *parser) funcDeclOrNil() *FuncDecl {
    if trace {
        defer p.trace("funcDecl")()
    }

    f := new(FuncDecl)
    f.pos = p.pos()
    f.Pragma = p.takePragma()

    if p.got(_Lparen) {
        rcvr := p.paramList(nil, _Rparen, false)
        switch len(rcvr) {
        case 0:
            p.error("method has no receiver")
        default:
            p.error("method has multiple receivers")
            fallthrough
        case 1:
            f.Recv = rcvr[0]
        }
    }

    if p.tok != _Name {
        p.syntaxError("expecting name or (")
        p.advance(_Lbrace, _Semi)
        return nil
    }

    f.Name = p.name()
    if p.mode&AllowGenerics != 0 && p.got(_Lbrack) {
        if p.tok == _Rbrack {
            p.syntaxError("empty type parameter list")
            p.next()
        } else {
            f.TParamList = p.paramList(nil, _Rbrack, true)
        }
    }
    f.Type = p.funcType()
    if p.tok == _Lbrace {
        f.Body = p.funcBody()
    }

    return f
}
```

其解析流程为：

1. 获取编译指令（Pragma） 因为编译指令在函数声明之前的注释中，执行到此时已经完成了解析
2. 当前词法解析器在 `func` 这个 Token 后面，如果当前遇到的是括号，那么说明这是一个方法声明，首先解析方法的 receiver
3. 解析函数名
4. 如果编译器开启了泛型支持（-G），并且下一个 Token 是 `[`, 则说明该函数包含类型参数，对其进行解析
5. &#x20;解析函数类型： 参数与返回列表封装到了一个表达式里面，称之为函数类型：

   ```
       type FuncType struct {
           ParamList  []*Field
           ResultList []*Field
           expr
       }
   ```

   &#x20;这样可以与形如 `type F func(int) int` 的类型声明共用数据结构
6. 解析完函数类型之后，如果此时 Token 是 `{` , 那么说明该函数有函数体，将其解析为 `BlockStmt` 语句。到此对函数声明的解析结束

在任何一步如果当前 Token 与期望的 Token 不一致，则说明出现了语法错误；否则该函数继续调用对应的解析函数完成解析，这种调用会一直递归下去，直到解析器遇到最基础的语句或者表达式为止。

本章开头处我们提到 Golang 采用的是递归下降的方式来进行语法解析，到此就可以体会到了，递归下降解析的优点是解析代码的结构与程序语法的结构几乎一致，这样的解析逻辑更加便于理解。

如果源文件没有语法错误的话，那么最终 `fileOrNil` 返回的结果 `*File` 就是该文件的 AST


# 3B.5 Unit Test及AST可视化

在了解了语法树的数据结构与基本解析思路之后，就没有必要继续钻入各个代码角落去学习解析细节了。此时更好的方式是反向学习：通过 UT 来调用解析函数，查看各种代码块的解析结果。

&#x20;语法分析器很容易进行单元测试，因为其本质上只需要一段代码作为输入，对运行环境没有额外的要求，因此我们不需要做太多的初始化工作。测试文件 parsertest.go 中已经包含了很多测试用例可供参考，可以直接运行。但在运行 UT 或者构建自己的 UT 之前，我们先来看一下如何更好的查看语法树的内容，毕竟其内嵌结构可能很深。

&#x20;文件 dumper.go 中的函数 `Fdump` 可以将一个 `Node` 的结构 dump 出来，而包括 `File` 在内，所有前面介绍过得结构体都实现了 `Node` 接口，所以该函数是我们可视化语法树的好工具。

&#x20;下面我们通过一些 UT 来进一步学习一下语法解析的功能。

&#x20;在 `parser_test.go` 内创建如下 UT

```go
func TestParser(t *testing.T) {
    code := `
package main

import (
"fmt"
"net/http"
)

const （a, b)                    
var names []string = make([]int, 10)

func iterate[T any](list []T, f func(T))  {
    for _, t  := range list {
        f(t)
    }
}
`

    ast, _ := Parse(NewFileBase("dump_source.go"), bytes.NewBuffer([]byte(code)), func(err error) {
        fmt.Printf("Parsing Error: %v\n", err)
    }, nil, CheckBranches|AllowGenerics) // 开启泛型支持，以便解析出类型参数

    if ast != nil {
        Fdump(os.Stdout, ast)
    }
}
```

&#x20;执行该 UT 得到输出结果：

> ```
> 1  *syntax.File {
> 2  .  Pragma: nil
> 3  .  PkgName: main @ dumpsource.go:2:9
> 4  .  DeclList: []syntax.Decl (5 entries) {
> 5  .  .  0: *syntax.ImportDecl {
> 6  .  .  .  Group: *syntax.Group {}
> 7  .  .  .  Pragma: nil
> 8  .  .  .  LocalPkgName: nil
> 9  .  .  .  Path: *syntax.BasicLit {
> 10  .  .  .  .  Value: “\”fmt\“”
> 11  .  .  .  .  Kind: 4
> 12  .  .  .  .  Bad: false
> 13  .  .  .  }
> 14  .  .  }
> 15  .  .  1: *syntax.ImportDecl {
> 16  .  .  .  Group: *syntax.Group {}
> 17  .  .  .  Pragma: nil
> 18  .  .  .  LocalPkgName: nil
> 19  .  .  .  Path: *syntax.BasicLit {
> 20  .  .  .  .  Value: “\”net/http\“”
> 21  .  .  .  .  Kind: 4
> 22  .  .  .  .  Bad: false
> 23  .  .  .  }
> 24  .  .  }
> 25  .  .  2: *syntax.ConstDecl {
> 26  .  .  .  Group: *syntax.Group {}
> 27  .  .  .  Pragma: nil
> 28  .  .  .  NameList: []*syntax.Name (2 entries) {
> 29  .  .  .  .  0: a @ dumpsource.go:9:8
> 30  .  .  .  .  1: b @ dumpsource.go:9:11
> 31  .  .  .  }
> 32  .  .  .  Type: nil
> 33  .  .  .  Values: nil
> 34  .  .  }
> 35  .  .  3: *syntax.VarDecl {
> 36  .  .  .  Group: nil
> 37  .  .  .  Pragma: nil
> 38  .  .  .  NameList: []*syntax.Name (1 entries) {
> 39  .  .  .  .  0: names @ dumpsource.go:10:5
> 40  .  .  .  }
> 41  .  .  .  Type: *syntax.SliceType {
> 42  .  .  .  .  Elem: string @ dumpsource.go:10:13
> 43  .  .  .  }
> 44  .  .  .  Values: *syntax.CallExpr {
> 45  .  .  .  .  Fun: make @ dumpsource.go:10:22
> 46  .  .  .  .  ArgList: []syntax.Expr (2 entries) {
> 47  .  .  .  .  .  0: *syntax.SliceType {
> 48  .  .  .  .  .  .  Elem: int @ dumpsource.go:10:29
> 49  .  .  .  .  .  }
> 50  .  .  .  .  .  1: *syntax.BasicLit {
> 51  .  .  .  .  .  .  Value: “10”
> 52  .  .  .  .  .  .  Kind: 0
> 53  .  .  .  .  .  .  Bad: false
> 54  .  .  .  .  .  }
> 55  .  .  .  .  }
> 56  .  .  .  .  HasDots: false
> 57  .  .  .  }
> 58  .  .  }
> 59  .  .  4: *syntax.FuncDecl {
> 60  .  .  .  Pragma: nil
> 61  .  .  .  Recv: nil
> 62  .  .  .  Name: iterate @ dumpsource.go:12:6
> 63  .  .  .  TParamList: []*syntax.Field (1 entries) {
> 64  .  .  .  .  0: *syntax.Field {
> 65  .  .  .  .  .  Name: T @ dumpsource.go:12:14
> 66  .  .  .  .  .  Type: any @ dumpsource.go:12:16
> 67  .  .  .  .  }
> 68  .  .  .  }
> 69  .  .  .  Type: *syntax.FuncType {
> 70  .  .  .  .  ParamList: []*syntax.Field (2 entries) {
> 71  .  .  .  .  .  0: *syntax.Field {
> 72  .  .  .  .  .  .  Name: list @ dumpsource.go:12:21
> 73  .  .  .  .  .  .  Type: *syntax.SliceType {
> 74  .  .  .  .  .  .  .  Elem: T @ dumpsource.go:12:28
> 75  .  .  .  .  .  .  }
> 76  .  .  .  .  .  }
> 77  .  .  .  .  .  1: *syntax.Field {
> 78  .  .  .  .  .  .  Name: f @ dumpsource.go:12:31
> 79  .  .  .  .  .  .  Type: *syntax.FuncType {
> 80  .  .  .  .  .  .  .  ParamList: []*syntax.Field (1 entries) {
> 81  .  .  .  .  .  .  .  .  0: *syntax.Field {
> 82  .  .  .  .  .  .  .  .  .  Name: nil
> 83  .  .  .  .  .  .  .  .  .  Type: T @ dumpsource.go:12:38
> 84  .  .  .  .  .  .  .  .  }
> 85  .  .  .  .  .  .  .  }
> 86  .  .  .  .  .  .  .  ResultList: nil
> 87  .  .  .  .  .  .  }
> 88  .  .  .  .  .  }
> 89  .  .  .  .  }
> 90  .  .  .  .  ResultList: nil
> 91  .  .  .  }
> 92  .  .  .  Body: *syntax.BlockStmt {
> 93  .  .  .  .  List: []syntax.Stmt (2 entries) {
> 94  .  .  .  .  .  0: *syntax.ForStmt {
> 95  .  .  .  .  .  .  Init: *syntax.RangeClause {
> 96  .  .  .  .  .  .  .  Lhs: *syntax.ListExpr {
> 97  .  .  .  .  .  .  .  .  ElemList: []syntax.Expr (2 entries) {
> 98  .  .  .  .  .  .  .  .  .  0: _ @ dumpsource.go:13:6
> 99  .  .  .  .  .  .  .  .  .  1: t @ dumpsource.go:13:9
> 100  .  .  .  .  .  .  .  .  }
> 101  .  .  .  .  .  .  .  }
> 102  .  .  .  .  .  .  .  Def: true
> 103  .  .  .  .  .  .  .  X: list @ dumpsource.go:13:21
> 104  .  .  .  .  .  .  }
> 105  .  .  .  .  .  .  Cond: nil
> 106  .  .  .  .  .  .  Post: nil
> 107  .  .  .  .  .  .  Body: *syntax.BlockStmt {
> 108  .  .  .  .  .  .  .  List: []syntax.Stmt (1 entries) {
> 109  .  .  .  .  .  .  .  .  0: *syntax.ExprStmt {
> 110  .  .  .  .  .  .  .  .  .  X: *syntax.CallExpr {
> 111  .  .  .  .  .  .  .  .  .  .  Fun: f @ dumpsource.go:14:3
> 112  .  .  .  .  .  .  .  .  .  .  ArgList: []syntax.Expr (1 entries) {
> 113  .  .  .  .  .  .  .  .  .  .  .  0: t @ dumpsource.go:14:5
> 114  .  .  .  .  .  .  .  .  .  .  }
> 115  .  .  .  .  .  .  .  .  .  .  HasDots: false
> 116  .  .  .  .  .  .  .  .  .  }
> 117  .  .  .  .  .  .  .  .  }
> 118  .  .  .  .  .  .  .  }
> 119  .  .  .  .  .  .  .  Rbrace: syntax.Pos {}
> 120  .  .  .  .  .  .  }
> 121  .  .  .  .  .  }
> 122  .  .  .  .  .  1: *syntax.ReturnStmt {
> 123  .  .  .  .  .  .  Results: *syntax.BasicLit {
> 124  .  .  .  .  .  .  .  Value: “\”unexpected\“”
> 125  .  .  .  .  .  .  .  Kind: 4
> 126  .  .  .  .  .  .  .  Bad: false
> 127  .  .  .  .  .  .  }
> 128  .  .  .  .  .  }
> 129  .  .  .  .  }
> 130  .  .  .  .  Rbrace: syntax.Pos {}
> 131  .  .  .  }
> 132  .  .  }
> 133  .  }
> 134  .  EOF: syntax.Pos {}
> 135  }
> ```

&#x20;但仔细查看我们所解析的代码，会发现其存在如下问题：

1. import 的库未使用
2. 常量 a, b 没有初始化
3. 变量 names 声明的类型与 make 中指定的类型不一致
4. iterate 声明中不需要返回值，但该函数却返回了一个字符串

&#x20;但解析器依然成功完成了解析，这是因为此时解析器只是负责做语法分析，只要程序符合语言的文法规范，解析器就能够顺利完成；而上述问题是语义问题，或者是代码规范问题（import 的库未使用），不属于语法解析器的管理范围。想要处理这些问题，编译器还需要将语法树继续往后传递并进行进一步处理。


# 4.1 简介

在程序中，任何一个表达式都包含两个维度的信息：一个是值，一个是类型。例如表达式 `"Golang"` ，其值是字符串 “Golang”, 而类型是 string. 通常情况下，我们将在编译阶段就知道所有类型信息的语言叫着静态语言，而直到运行时才知道类型信息的语言叫着动态语言。

语言的类型规范构成了语言的类型系统，类型系统是对程序行为进行约束的重要措施，类型规范通常会涉及到如下几个方面：

1. 类型种类，例如数字类型、布尔类型、复合类型等；类型种类用来限定类型的范围与界限
2. 预定义的类型，例如常见的 int, float, string 等；用来定义语言的基础类型
3. 构造新类型的规则，用户如何通过已有类型构造出新的类型；形成创建新类型的语法
4. 类型之间的相互关系，例如是否可以相互转换以及相互转换的规则
5. 作用于不同类型的运算符及其规则，例如大多数语言中， `+` 可以作用于数字类型，也可以作用于字符串类型，而 `/` 则无法作用于后者

有了这些规则，用户就可以根据自己的需求构造出任何想要的类型，以便搭建出业务的抽象模型。而语言的类型系统会检查程序的类型是否符合约束，对于静态语言，这个检查发生在编译阶段，而动态语言则发生在运行阶段。静态语言在使用时会伴随着大量的类型申明，例如 `var name string`, 不仅声明了变量名，同时为其指定了类型；而动态语言则无此必要，程序会在运行时将类型信息与值进行绑定。静态语言更加严格，但能够在编译阶段提前发现错误；动态语言更加灵活，但也将问题发现的时机推迟到了运行时刻。

为了使语言的使用更加简便，很多静态语言对类型的声明也不是必须的，只要编译器能够根据上下文推断出实际的类型，就没有必要让用户进行显示地声明，这种能力叫着类型推导。例如我们有函数 `func sum(i, j int) int` , 则在赋值语句 `s = sum(1, 2)` 中，很明显可以推导出变量 `s` 的类型便是函数 `sum` 的返回类型。Go 就具备这种语法特性，在编译阶段， Go 编译器会做两方面的事情：一个是类型推导，一个是类型检查，负责该任务的模块叫着类型检查器（type checker）。接下来我们会详细分析 Go 的类型检查器的实现原理。


# 4.2 代码结构

## 4.2.1 入口简介

{% hint style="info" %}
在[前言](/)中提到 Go 的编译器团队当前正在重写类型检查器，虽然新版本的代码已经合并到 master 分支，但当前默认仍旧使用的是老版本的类型检查器，新的类型检查器可以通过编译参数 "-G=2" 使用，并且应该会随着泛型的发布而默认启用，因此本文将完全基于新代码进行分析。

老的类型检查代码的入口位置与新的代码位置邻近，相信对老系统感兴趣的同学在阅读完本章之后，也能够无障碍地找到对应的实现，所以本文将不会对老代码做特别的说明。
{% endhint %}

### 代码入口&#x20;

类型检查的输入是语法分析的输出 --- AST，其代码入口位置就在语法解析之后，同在`$GCROOT/compile/internal/noder/noder.go`文件内的函数`LoadPackage()`中。解析器处理完语法解析的错误信息之后，随即进行类型检查：

```go
if base.Flag.G != 0 { // 编译器通过参数 -G 开启新类型检查
    // Use types2 to type-check and possibly generate IR.
    check2(noders) // 类型检查入口函数
    return
}

```

check2 在文件`$GCROOT/compile/internal/noder/irgen.go`中定义，该函数实际上做了两件事情：类型检查；然后生成 IR(Intermediate Representation) Tree，我们当前仅关注前者，下一章将详细介绍如何生成 IR Tree。

### 主体代码位置

类型检查的逻辑非常复杂，涉及到的主体代码都在目录`$GCROOT/compile/internal/types2`下，其中对常量表达式的处理又依赖`$GOROOT/src/go/constant`与`$GOROOT/src/go/token`

{% hint style="info" %}
Go 语言通过包`$GOROOT/src/go` 将词法分析器、语法分析器、包加载器（Importer）以及类型检查器作为库开放了出来，以提供给需要对 go 语言进行分析的第三方工具使用，像gopls, guru等工具都使用该包来对 go 的源代码进行分析，甚至编译器本身也对该库有依赖。

就核心功能而言，该包的功能基本上是编译器完整功能的一个子集，代码也有很大部分的重复，但是因为该包需要保持稳定以及向前兼容，而编译器的开发非常活跃，所以 Go 团队还是保持了两份代码，虽然违背了工程上的一些准则，但从实践上来说维护起来更加简单。
{% endhint %}

## 4.2.2 索引文件

新类型检查器的所有代码都在目录`$GCROOT/compile/internal/types2/`下，各文件及功能简介如下：

* examples: 泛型使用示例
* testdata: 各种测试用例，例如各种循环引用、类型检查等
* api.go: 类型检查器对外提供的重要接口类数据结构及方法，例如 Info, Config, Identical() 等
* assignments.go: 处理赋值语句的检查工作，检查一个类型的值能够赋给另一个类型；常量、变量的初始化函数也在该文件内
* builtins.go: 检查对内置函数的调用是否合法&#x20;
* call.go: 对函数、方法调用进行类型检查
* check.go: 整个类型检查的主控逻辑
* conversions.go: 对类型转换进行类型检查，例如 A(b)
* decl.go: 对单个 Object 对象进行类型检查的主控逻辑
* errors.go: 类型检查错误处理逻辑，包括 error 定义与一些通用逻辑定义
* expr.go: 对表达式进行类型检查
* infer.go: 根据实际类型对类型参数进行推导
* initorder.go: 构建初始化顺序
* labels.go: 对程序中 label 的使用是否正确进行检查
* lookup.go: 属性、方法查找逻辑，以及判断给定类型是否实现了特定接口
* object.go: 定义 Object 对象，该对象是类型检查展开的核心数据结构
* operand.go: 定义数据结构 operand, 该结构用来封装类型检查过程中的操作数。该文件还包括判断一个 operand 能够赋值给指定类型的方法
* package.go: 定义了 Package 数据结构及辅助方法
* pos.go: 主要用来计算给定 syntax.Node 在源文件中的位置，包括开始位置及结束位置
* predicates.go: 定义了很多对类型各种性质进行判断的断言函数，例如 isBoolean(), isOrdered() 等；最重要的是判断两个类型是否等价的 identical() 函数
* resolver.go: 类型检查的预处理函数及入口函数，基本用于辅助 check.go 中的函数
* return.go: 判断语句是否包含 return, 是否包含 break 等
* scope.go: 定义作用域
* selection.go: 定义并处理形如 X.sel 这类表达式的逻辑
* stmt.go: 对程序内的语句进行类型检查，例如函数体
* subst.go: 对包含泛型的类型进行实例化，即根据实际的类型参数创建具体的类型
* type.go: 定义与所有类型相关的数据结构
* typexpr.go: 根据类型表达式创建具体类型，例如根据 \~\[]int\~ 创建出切片类型
* unify.go: 判断包含泛型的两个类型是否等价
* universe.go: 初始化 Universe 全局作用域


# 4.3 符号解析

类型检查最重要的一件事情就是做符号解析，所谓符号解析，就是将程序中通过“名字”所引用的真实对象找出来。这里所说的“名字”，在词法分析阶段对应的 Token 类型是`_Name`, 而在语法阶段对应的表达式是`Name`，其定义如下：

```go
// file: $GCROOT/compile/internal/syntax/nodes.go
type Name struct {
	Value string
	expr
}

```

名字所引用的对象就是语言中某个类型的具体实例，例如函数、变量等。例如下列代码涉及到的名字包括：A, main, a, name, fmt, Printf.

```go
import "fmt"

type A struct{}

func main() {
	var a A
	name := "Golang"

	fmt.Printf("%v-%s\n", a, name)
}

```

go 的编译单位是 package, 即每次编译的源文件必须属于同一个包，所以涉及到的符号来源可以简单归为如下几类：&#x20;

1. 预定义符号，即内置符号，包括语言的内置类型、内置函数、泛型的内置 constraints 等
2. 当前源文件所依赖的包，即通过 \~import\~ 语句引用的各个 package
3. 当前源文件自定义的符号，即定义的类型、变量等

为了完成符号解析，编译器需要构建一个符号表，其在编译的初始化阶段会注册第一部分的符号，在类型检查时导入第二部分的符号，并在实际类型检查过程中对第三部分的符号进行处理。除了符号注册，编译器还需要对符号的作用域进行管理，即处理好哪些符号是 public 的，哪些是 package local 的，哪些是 function local 的。为了管理好这些逻辑，编译器需要定义各种数据结构来抽象对应的概念，我们将在下一节详细分析类型检查时所涉及到的重要数据结构。


# 4.4.1 数据结构 - 作用域

作用域是编程语言的一个重要课题，go 语言采用的是静态作用域，即词法作用域。在编译器内部，根据作用域范围从大到小将其划分为：

1. 全局作用域，该作用域内的对象在任何地方都可以访问，编译器中叫 `Universe`
2. 包作用域
3. 文件作用域，该作用域只在编译器内部使用，语言层面没有体现
4. 块作用域，简单说即 `{}` 包围起来的代码片段，例如函数体、控制语句块（例如 if, for 等内部）

编译器采用树形结构来管理作用域，对应的数据结构定义在文件 $GCROOT/compile/internal/types2/scope.go 中：

```go
type Scope struct {
    parent   *Scope            // 当前作用域的父作用域
    children []*Scope          // 当前作用域的子作用域
    elems    map[string]Object // 当前作用域所包含的符号对象，key 是符号名称
    pos, end syntax.Pos        // 作用域位置信息
    comment  string            // for debugging only
    isFunc   bool              // set if this is a function scope (internal use only)
}
```

`Scope` 定义了插入与查找符号的方法：

```go
func (s *Scope) Lookup(name string) Object                                 { /* 忽略函数体 */ }
func (s *Scope) LookupParent(name string, pos syntax.Pos) (*Scope, Object) { /* 忽略函数体 */ }
func (s *Scope) Insert(obj Object) Object                                  { /* 忽略函数体 */ }
```

`Lookup` 仅在本作用域内查找符号， `LookupParent` 则从当前作用域开始，顺着父作用域一直查找，这两个方法是进行符号解析的 API。 `elems` 用来存放当前作用域内名字到符号对象的映射，符号对象是实现了 `Object` 接口的结构，在后续会专门介绍。


# 4.4.2 数据结构 - Package

package 的数据结构定义在文件`$GCROOT/compile/internal/types2/package.go`中：

```go
type Package struct {
	path     string     // 包的引用路径，例如 import "fmt" 中的 "fmt"
	name     string     // 包名，即代码中 package xxx 声明的内容
	scope    *Scope     // 包作用域，该包的 public 符号对象都在该作用域内
	complete bool       // 如果该包的 scope 内包含了所有的 public 符号对象，则为 true, 否则为 false
	imports  []*Package // 该包的依赖
	fake     bool
	cgo      bool
}
```

包通常以对象文件（.o 或者 .a 文件）的形式存在于文件系统中，而`Package`则对应一个对象文件在编译器中的内存结构。在编译阶段，包加载器负责完成包的加载与解析，包内符号对象都在`scope`属性中，下文[包加载器](/golang-bian-yi-qi-lei-xing-jian-cha/lei-xing-jian-cha-luo-ji)一节会对加载逻辑做详细介绍。


# 4.4.3 数据结构 - Object 对象

`Object`定义了一个接口，用来表示程序中的各类命名实体，即前文一直提到的符号对象，例如函数、变量、类型等。编译器内部为不同的符号类型定义了不同的数据结构，所有定义都在文件 `$GCROOT/compile/internal/types2/object.go` 中。

## 4.4.3.1. 接口及公用结构

接口 `type Object interface{...}` 的声明如下：

```go
// An Object describes a named language entity such as a package,
// constant, type, variable, function (incl. methods), or label.
// All objects implement the Object interface.
// 
type Object interface {
	Parent() *Scope  // scope in which this object is declared; nil for methods and struct fields
	Pos() syntax.Pos // position of object identifier in declaration
	Pkg() *Package   // package to which this object belongs; nil for labels and objects in the Universe scope
	Name() string    // package local object name
	Type() Type      // object type
	Exported() bool  // reports whether the name starts with a capital letter
	Id() string      // object name if exported, qualified name if not exported (see func Id)

	// String returns a human-readable string of the object.
	String() string

	// order reflects a package-level object's source order: if object
	// a is before object b in the source, then a.order() < b.order().
	// order returns a value > 0 for package-level objects; it returns
	// 0 for all other objects (including objects in file scopes).
	order() uint32

	// color returns the object's color.
	color() color

	// setType sets the type of the object.
	setType(Type)

	// setOrder sets the order number of the object. It must be > 0.
	setOrder(uint32)

	// setColor sets the object's color. It must not be white.
	setColor(color color)

	// setParent sets the parent scope of the object.
	setParent(*Scope)

	// sameId reports whether obj.Id() and Id(pkg, name) are the same.
	sameId(pkg *Package, name string) bool

	// scopePos returns the start position of the scope of this Object
	scopePos() syntax.Pos

	// setScopePos sets the start position of the scope for this Object.
	setScopePos(pos syntax.Pos)
}

```

该接口申明的方法比较多，但基本都是属性的 Getter 或者 Setter 方法，公用信息定义在结构`type object struct{...}`中，具体声明如下：

```go
type object struct {
	parent    *Scope
	pos       syntax.Pos
	pkg       *Package
	name      string
	typ       Type
	order_    uint32
	color_    color
	scopePos_ syntax.Pos
}
```

object 完整地实现了`Object`接口。其中`color_`属性用于后期的[对象循环依赖检查](/golang-bian-yi-qi-lei-xing-jian-cha/4.5.32a-dui-xiang-xun-huan-yi-lai-jian-cha)，而`typ`属性用来保存该对象的实际类型，类型推导目的就是要为每个对象推导出该属性，[类型数据结构](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.1-shu-ju-jie-gou-zuo-yong-yu#32-lei-xing-shu-ju-jie-gou)一节将会详细介绍各种类型的数据结构。

## 4.4.3.2. 具体Object类型

编译器一共定义了 8 类 Object, 包括：

* PagName: 包对象
* Const: 常量对象
* TypeName: 代表通过`type`关键字定义的类型，或者类型别名
* Var: 代表一个变量声明，通过用来表示函数的参数、返回值，以及结构体的 Field
* Func: 代表一个方法、函数声明，或者接口内的方法声明
* Label: 代表程序内的一个 label, 用于 break, goto 等关键字；其没有 type 属性
* Builtin: 代表一个内置函数
* Nil: 代表 nil 值
* dependency: 用来标识初始化表达式可能依赖的对象，只有`Const`, `Var`, 以及`Func`是合法的该类对象。该类对象用于构建全局变量的初始化顺序，这在后面会有详细介绍。

每种类型的自定义属性并不多，基本都只是复用了 object, 所以在这里就不一一列举代码了，详情请读者自行查阅`$GCROOT/compile/internal/types2/object.go`。

## 4.4.3.3. Object对象的等价性

每个 Object 对象都代表着程序的一个符号，所以只有指向同一个符号的两个 Object 对象才是等价的，而在不同作用域的两个符号即使在结构上完全一样，也是不同的对象，这一点与类型的等价规则不同。[类型的等价规则](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.412-lei-xing-de-deng-jia-gui-ze-fl)会在后面详细介绍。

## 4.4.3.4. Object的作用

Object 对象是类型检查的重要数据结构，准确地说，该对象的内部封装了符号对象的各方面信息：包、作用域、类型、位置信息等；每一个 Object 都与 AST 中的一个节点（Node）对应，类型检查器保存了这两类信息的映射关系（[Checker](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.5-lei-xing-jian-cha-qi#3-checker) 的 objMap 属性）。

类型检查的目的，就是将所有 Object 对象的类型属性，即 object 中的 typ 属性推导出来，并检查整个类型系统的兼容性。事实上，类型检查的整个逻辑都是围绕着 Object 对象展开的，我们会在[类型检查逻辑](/golang-bian-yi-qi-lei-xing-jian-cha/lei-xing-jian-cha-luo-ji)一节对其进行详细分析。在此之前，我们先来熟悉一下编译器内部的类型数据结构。


# 4.4.4-1 类型数据结构 - 简介

既然编译器要进行类型检查，那么必然就需要为各种类型定义数据结构，所有与类型相关的结构都定义在文件`$GCROOT/compile/internal/types2/type.go`中，其中有的数据结构直接与 go 的语法块相对应，有的则是编译器内部封装的结构体。该数据结构是类型检查的核心，我们多花点篇幅逐个查看。


# 4.4.4-2 类型接口

首先是`Type`接口，定义如下：

```go
type Type interface {
    // Underlying returns the underlying type of a type
    // w/o following forwarding chains. Only used by
    // client packages (here for backward-compatibility).
    Underlying() Type

    // String returns a string representation of a type.
    String() string
}
```

不同类型之间的差异性很大，所以类型接口的方法只有两个，该接口也是`Object`接口中`Type()`方法的返回类型。


# 4.4.4-3 基础类型

Go 语言内置的基础类型包括：

* 布尔类型
* 数字类型，例如整数、浮点数、复数等
* 字符串类型

编译器使用同一个结构来表示所有的基础类型，其定义如下：

```go
// A Basic represents a basic type.
type Basic struct {
    kind BasicKind
    info BasicInfo
    name string
}
```

`BasicInfo`用来描述类型属性，例如该类型是否是数字类型，是否可排序。其定义如下：

```go
type BasicInfo int

// Properties of basic types.
const (
    IsBoolean BasicInfo = 1 << iota
    IsInteger
    IsUnsigned
    IsFloat
    IsComplex
    IsString
    IsUntyped

    IsOrdered   = IsInteger | IsFloat | IsString
    IsNumeric   = IsInteger | IsFloat | IsComplex
    IsConstType = IsBoolean | IsNumeric | IsString
)
```

`BasicInfo`用来标识类型的性质，对应的常量申明已经解释了其潜在用途。

`BasicKind`用来标识实际类别，例如`Int32`, `Float64`等；这里简单罗列几个以做示例：

```go
// BasicKind describes the kind of basic type.
type BasicKind int

const (
    Invalid BasicKind = iota // type is invalid

    // predeclared types
    Bool
    // ... 省略其它类型
    Int8
    Complex128
    String
    UnsafePointer

    // types for untyped values
    UntypedBool
    UntypedInt
    UntypedRune
    UntypedFloat
    UntypedComplex
    UntypedString
    UntypedNil

    // aliases
    Byte = Uint8
    Rune = Int32
)
```

可以看出对于每一个基础类型，都有一个枚举类型与之对应，另外可以发现 go 中的`byte`与`rune`类型只是`uint8`与`int32`的类型别名。

这里稍稍多讲一下 Untypedxxx 这一组类型。go 的类型系统非常严格，如果一个表达式的类型是确定的，那么不管在什么场景下，编译器都不会做任何隐式的类型转换，例如如下代码：

```go
var a int16
var b int32

c := a + b // 错误信息：invalid operation: mismatched types int16 and int32
```

在赋值语句`c := a + b` 中，a与b的类型都是确定的整数类型，而将 a 转换为 int32 也是安全的，但 go 并不会这么做。反观 Java 则比较灵活，其定义了一组类型的隐式转换规则，例如如下 Java 代码是合法的：

```java
int a = 10;
float b = 1.0f;
String c = "Java";
String d = a + b + c; 
```

但是 go 也没有死板到无可救药，对于类型还不确定的表达式，go 会根据上下文来进行类型转换。但 go 中仅允许常量在声明时拥有不确定的类型，对应`BasicKind`中`Untypedxxx`的几类。

更准确地说，go 基础类型的字面量都是 untyped 的，例如 "golang", 12, 3.14 分别对应 UntypedString, UntypedInt, UntypedFloat. 当使用这些字面量进行操作，例如赋值、运算、作为参数传递时，编译器会根据上下文将其转换为目标类型。我们来看几个示例：

* 常量声明有类型

```java
const name string = "Golang"
```

"Golang" 类型为 UntypedString, 但是 name 的声明中指定类型为 string, 所以编译器在初始化 name 时会将 "Golang" 转换为 string 类型并且赋值给 name. 如果此时类型转换不匹配，则会报错：

```go
const name string = 1.2 // 错误信息：cannot convert 1.2(untyped float constant) to string
```

* 常量声明无类型

```go
const name = "Golang"
const alias string = name
```

name 没有申明类型，所以其类型复用字面量 "Golang" 的类型 UntypedString; 而 alias 的类型是 string, name 的值赋给 alias 的时候编译器会将其转换为 string 类型

* 根据常量 underlying type 进行转换

```go
type Str string

const name = "Golang"
const alias Str = name

const owner string = "google"
const ownerAlias Str = owner // 错误信息：cannot use owner (constant "google" of type string) as Str value in constant declaration
```

这里我们声明了新的类型 Str, 其 underlying type 是 string, 通过对 alias 的赋值语句可以看出：编译器依然可以将 UntypedString 转换为 Str 类型。但 owner 被显示声明成了 string 类型，其与 Str 毕竟是不同的类型，所以 ownerAlias 的赋值语句中，编译器不会将 string 类型的 owner 隐式转换为 Str 类型。

* 常量声明无类型

```go
type Str string

var name = "Golang"
var alias Str = name // 错误信息：cannot use name (variable of type string) as Str value in variable declaration
```

上面我们提到 go 仅仅支持常量拥有不确定类型，所以即使变量 name 没有申明类型，但编译器会根据右边的值推测出一个具体的类型赋给 name, 这里 "Golang" 的类型是 UntypedString。UntypedString 只能被转换为 string 类型，所以对 alias 的赋值会报错。

Untyped 类型的转换规则很简单：数字类型低精度可以向高精度转换，除此之外只有相同的类型才能相互转换。


# 4.4.4-4 内置复合类型

除了基础类型之外，Go 语言规范中还定义了多种复合类型，编译器为每种类型都定义了对应的数据结构

## 4.4.4-4.1. 数组

数组类型（Array）的结构定义如下：

```go
type Array struct {
    len  int64
    elem Type
}
```

数组是定长的数据容器，所以除了元素类型之外，数组长度也是其类型中的组成部分，因此`[2]int`与`[3]int`虽然都是整型数组，但对类型系统而言，他们属于不同的类型。

## 4.4.4-4.2. 切片

切片类型（Slice）的结构定义如下：

```go
type Slice struct {
    elem Type
}
```

切片是变长的数据容器，其类型定义中不包括长度信息。

## 4.4.4-4.3. Map

Map 类型的结构定义如下：

```go
type Map struct {
    key, elem Type
}
```

其中`key`必须是可比较的数据类型（Comparable Type），后文中[类型的比较规则](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.413-lei-xing-de-bi-jiao-gui-ze)有详细介绍。

## 4.4.4-4.4. Channel

Channel 类型的结构定义如下：

```go
type Chan struct {
    dir  ChanDir
    elem Type
}

// A ChanDir value indicates a channel direction.
type ChanDir int

// The direction of a channel is indicated by one of these constants.
const (
    SendRecv ChanDir = iota
    SendOnly
    RecvOnly
)
```

其中一个是 channel 的元素类型，一个是 channel 的数据流方向。

## 4.4.4-4.5. 指针

指针类型的结构定义如下：

```go
type Pointer struct {
    base Type // element type
}
```


# 4.4.4-5 Struct 类型

struct 用来让用户自定义类型，其数据结构定义如下：

```go
type Struct struct {
    fields []*Var
    tags   []string // field tags; nil if there are no tags
}
```

该类型记录着通过 struct 关键字定义的类型信息，每个 Field 都是一个 [Var对象](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.3-shu-ju-jie-gou-object-dui-xiang#2-ju-ti-object-lei-xing)，该对象的`IsField()`方法返回 true.


# 4.4.4-6 Interface 类型

Interface 类型的结构定义如下：

```go
type Interface struct {
	methods   []*Func // 接口内显示声明的方法集合，按照 Func 的 less 方法定义的顺序排序
	types     Type    // 当 interface 用于泛型的 constraint 时，用来记录限定的类型集合，当前实现是 Sum 类型
	embeddeds []Type  // 内嵌的接口类型

	allMethods []*Func // 所有内嵌类型的方法与自身方法的全集
	allTypes   Type    // 当通过 type 关键字申明 constraint 类型时，该字段用来保存所有内嵌类型的 constraint 与自身 constraint 的交集。用于泛型

	obj Object // type declaration defining this interface; or nil (for better error messages)
}
```

从语义上来说，一个接口可能包含一组方法声明、一组内嵌的接口类型、以及一组泛型的 constraints。因为 go 的接口类型非常重要，类型检查器还专门定义了一些用于接口类型的辅助方法：

```go
// $GCROOT/compile/internal/types2/api.go
// 判断类型 V 是否实现了接口 T
func Implements(V Type, T *Interface) bool { /* 忽略函数体 */ }

// $GCROOT/compile/internal/types2/api.go
// 判断 V.(T) 是否合法
func AssertableTo(V *Interface, T Type) bool { /* 忽略函数体 */ }

// $GCROOT/compile/internal/types2/lookup.go
// 该方法用来判断类型 V 的方法集合是否是接口 T 方法集合的子集，如果不是，则返回第一个在 V 中但不在 T 中的方法。该方法用来实现上文的 Implements 方法
func MissingMethod(V Type, T *Interface, static bool) (method *Func, wrongType bool) { /* 忽略方法体 */
}
```


# 4.4.4-7 Named 类型

Named 结构用来表示 Defined Types, 即通过语法结构`type A β`或者`type A = β`定义的类型，其中 β 表示任何合法的表达式，例如如果 β 表示`struct {...}`, 则整个声明就是一个 struct 结构体。

{% hint style="info" %}
`type A = β`叫着类型别名，是 go 1.9 引入的语法，通常情况下这种用法中 β 都是一个具体的类型名字，假设为 B，则此时 A 与 B 完全是一样的类型，纯粹只是为类型 B 引入一个别名。可以参考[Russ Cos 的文章](https://talks.golang.org/2016/refactor.article)了解使用场景。

而`type A β`则会创建一个新的类型，A 的 underlying type 是 β 所表示的类型, 例如如果 β 表示`struct {...}` , 则其类型就是上文提到的 Struct 类型；如果 β 是一个具体的类型名字，例如 C, 那么 A 的 underlying type 就是 C. 后面有专门的章节讨论Underlying Type.
{% endhint %}

Named结构定义如下：

```go
type Named struct {
	check      *Checker // for Named.under implementation
	info       typeInfo
	obj        *TypeName   // corresponding declared object
	orig       Type        // type (on RHS of declaration) this *Named type is derived of (for cycle reporting)
	underlying Type        // possibly a *Named during setup; never a *Named once set up completely
	tparams    []*TypeName // 类型参数名字，用于泛型
	targs      []Type      // 类型参数的限定类型（constraint）
	methods    []*Func     // 与该类型绑定的方法
}
```

check 是类型检查器的数据结构，详细介绍在[Checker](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.5-lei-xing-jian-cha-qi#3-checker), 字段`info`在后期用于类型的[循环依赖检查](/golang-bian-yi-qi-lei-xing-jian-cha/4.5.31.3e-chu-li-delayed-dui-lie#xun-huan-yi-lai-jian-cha). 每种类型都必须实现`Underlying() Type`方法（见[Type 接口](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.42-lei-xing-shu-ju-jie-gou-jie-kou)），其他所有类型的该方法都是返回自身，只有 Named 类型是返回属性`underlying`.


# 4.4.4-8 Tuple 类型

Tuple 类型用来表示一个已排序的变量对象，其定义如下：

```go
type Tuple struct {
	vars []*Var
}
```

Tuple 通常用来表示函数的参数列表、返回值列表等，该类型仅仅用于编译器内部，不是语言层面的类型。


# 4.4.4-9 Sum 类型

Sum 类型用来表示一个类型集合，其定义如下：

```go
type Sum struct {
	types []Type // types are unique
}
```

Sum 当前用在`Interface`类型的 types 属性中，用于表示泛型的限定类型列表（constraint list）。该类型也是编译器内部定义的辅助类型结构，不是语言层面的类型。


# 4.4.4-10 Function & Method 类型

函数与方法的类型使用同一个结构来表示，其定义如下：

```go
type Signature struct {
	rparams  []*TypeName // 如果是方法，存放 receiver 的泛型列表
	tparams  []*TypeName // 存放方法的泛型列表
	scope    *Scope      // 函数所在的作用域
	recv     *Var        // 如果是方法，则存放 receiver, 否则为 nil
	params   *Tuple      // 参数类型列表
	results  *Tuple      // 返回值类型列表
	variadic bool        // 如果左右一个是变长参数（...T），则为 true
}
```


# 4.4.4-11 泛型类型

&#x20;函数与类型中申明的泛型使用如下数据结构来表示：

```go
type TypeParam struct {
    check *Checker  // for lazy type bound completion
    id    uint64    // unique id
    obj   *TypeName // 
    index int       // parameter index
    bound Type      // *Named or *Interface; underlying type is always *Interface
}
```

&#x20;除此之外还有三个类型用于泛型，其定义如下：

```go
type instance struct {
    check   *Checker     // for lazy instantiation
    pos     syntax.Pos   // position of type instantiation; for error reporting only
    base    *Named       // parameterized type to be instantiated
    targs   []Type       // type arguments
    poslist []syntax.Pos // position of each targ; for error reporting only
    value   Type         // base(targs...) after instantiation or Typ[Invalid]; nil if not yet set
}
type bottom struct{}
type top struct{}
```

`bottom` 与 `top` 是两个标记类型。当接口中包含泛型的 constraints 时，我们需要推算出当前接口中 constraints 所包含的实际类型，例如如下申明：

```go
type SmallInt interface {
    type int8, int16, int32
}

type BigInt interface {
    type int, int32, int64
}

type MediumInt interface {
    SmallInt
    BigInt
    type int16, int32
}
```

`MediumInt` 中除了申明了自己的 constraints 列表, 还内嵌了 `SmallInt` 与 `BigInt`, 此时 `MediumInt` 的实际 constraints 自然应该是三者的并集：int32. `bottom` 与 `top` 两个类型用于表示两个特殊的集合：前者表示空集，后者表示全集。

instance 代表一个实例化了的泛型，泛型也叫类型参数（type parameter），我们先来回顾一下普通的函数参数：

```go
func sum(i, j int) int {
    return i + j
}
```

但凡说道“参数”，就需要使用者将具体的实参传递进去。i, j 是函数 sum 的形参，我们可以将函数看着是一个计算模版，当调用者提供该模版的实参时，我们就可以通过该“模版”计算出最终值。那么“类型参数”该如何传递呢？我们先来看一个典型的泛型定义：

```go
type Node[T comparable] struct {
    val T
}
```

Node 是我们定义的一个类型，但是该类型并不完备：属性 val 的类型 T 还是未知的，由使用者提供。我们可以将 T 看着是 Node 的类型参数，从而将将 `Node[T comparable]` 看着是一个类型模版，当使用者提供具体的参数类型时，我们就可以得到一个具体的Node类型。例如当 T 为 int32 时，我们得到具体类型是：

```go
type Node struct {
    val int32
}
```

该过程叫着类型的实例化。所以在代码中，任何使用泛型 Node 的地方都需要传入类型参数，例如如下代码：

```go
// 该方法中一共有三个实例化的 Node 类型：Node[string], Node[float32] 与 Node[int32]
func convert(nod Node[string]) Node[float32] {   
var _ Node[int32]
}
```

go 编译器内部使用 `instance` 来表示一个实例化的泛型类型，其中 `base` 是“类型模版”，而 `targs` 就是具体传入的参数类型值。例如上述代码中将会生成三个具体类型，对应三个 instance 实例。

事实上，编译器在[生成 IR Tree](/golang-bian-yi-qi-ir-tree/untitled-2#org3e1ddf1) 时便会为所有泛型函数做实例化。


# 4.4.4-12 类型的等价规则

Go 语言采用的是[结构化类型系统](https://en.wikipedia.org/wiki/Structural_type_system), 也就是说判断两个类型是否相等考虑的是类型的内在结构，而与类型的名字或具体的声明位置无关。判断两个类型是否等价的入口函数定义在`$GCROOT/compile/internal/type2/api.go`中：

```go
func Identical(x, y Type) bool { /* ... */ }
```

详细逻辑在`$GCROOT/compile/internal/types2/predicates.go`中：

```go
func (check *Checker) identical0(x, y Type, cmpTags bool, p *ifacePair) bool {
	// types must be expanded for comparison
	x = expandf(x)
	y = expandf(y)

	if x == y {
		return true
	}

	switch x := x.(type) {
	case *Basic:
		// Basic types are singletons except for the rune and byte
		// aliases, thus we cannot solely rely on the x == y check
		// above. See also comment in TypeName.IsAlias.
		if y, ok := y.(*Basic); ok {
			return x.kind == y.kind
		}

	case *Struct:
		// Two struct types are identical if they have the same sequence of fields,
		// and if corresponding fields have the same names, and identical types,
		// and identical tags. Two embedded fields are considered to have the same
		// name. Lower-case field names from different packages are always different.
		if y, ok := y.(*Struct); ok {
			if x.NumFields() == y.NumFields() {
				for i, f := range x.fields {
					g := y.fields[i] // 对应位置的属性
					if f.embedded != g.embedded ||
						cmpTags && x.Tag(i) != y.Tag(i) ||
						!f.sameId(g.pkg, g.name) ||
						!check.identical0(f.typ, g.typ, cmpTags, p) {
						return false
					}
				}
				return true
			}
		}

	// 以下每个 case 内部的代码都省略了，详细实现参考源文件
	case *Array:
	case *Slice:
	case *Pointer:
	case *Tuple:
	case *Signature:
	case *Sum:
	case *Interface:
	case *Map:
	case *Chan:
	case *Named:
	case *TypeParam:
	case *bottom, *top:
	case nil:
	default:
		unreachable()
	}

	return false
}
```

该函数内部的每个 case 对应一种类型，然后使用递归的方式检查类型的内部结构是否一致，我们留下Basic及Struct类型来做参考。两个Basic如果是同一种类型，则彼此等价；而两个Struct类型等价需要符合如下条件：&#x20;

1. 属性数目相同，并且拥有相同的序列&#x20;
2. 对于同一个位置的两个属性，其名称相同，类型相同，并且 tag 相同&#x20;
3. 如果两个 struct 在不同的包中定义，则不能包含私有属性

我们看如下代码：

```go
// package user
type User struct { // User 定义在 package user 中
	Name string
}

// package main
type People struct { // People 定义在 package main 中，其内部结构与 user.User 一致
	Name string
}

func buildProfile(u struct { // 函数的参数类型是一个无名类型，其结构与 user.User、People 均相同
	Name string
}) {
	fmt.Printf("%v\n", u)
}

func main() {
	// OK, 将 user.User 作为函数参数是合法的
	buildProfile(user.User{})

	// OK, 将 People 作为函数参数是合法的
	buildProfile(People{})

	// OK, 将无名类型字面量作为函数参数是合法的
	buildProfile(struct {
		Name string
	}{
		Name: "Jerry",
	})

	// OK, 可以直接将 user.User 类型转换为 People 类型
	var _ user.User = user.User(People{})
}
```

通过上述代码可以发现，只要类型的结构是一致的，那么就是等价的。

{% hint style="info" %}
除了结构化类型系统之外，在编程语言中常用的类型系统还包括[Nominal Type System](https://en.wikipedia.org/wiki/Nominal_type_system)以及[Duck Typing](https://en.wikipedia.org/wiki/Duck_typing), 前者根据类型声明的位置以及名字来区分（Java），而后者根据类型的行为来区分（Python）。

随着对泛型支持，类型等价性比较会变得更加复杂，这在后面[方法的等价性校验](/golang-bian-yi-qi-lei-xing-jian-cha/4.5.32b-fang-fa-yu-shu-xing-cha-zhao#fang-fa-de-deng-jia-xing-xiao-yan)有更详细的介绍，现在该功能还在开发之中，可能后期完成之后会合并到一起。
{% endhint %}


# 4.4.4-13 类型的比较规则

除了类型的等价性，程序中还经常需要对比两个实例是否相等，或者其大小关系，这类操作通过比较运算符来实现。go 定义了 6 个[比较运算符](https://golang.org/ref/spec#Comparison_operators)（==, !=, >=, <=, >, <）, 其中 != 与 == 应用于可比较类型（Comparable type），而剩下四个运算符应用于可排序类型（Ordered type）；Ordered type 都是与数字、字符串相关的基础类型，在[基础类型的 BasicInfo](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.43-lei-xing-shu-ju-jie-gou-ji-chu-lei-xing)中有明确定义。Comparable type 指的是那些可以判断不同实例是否相等的类型，例如 map 的 key, 就只能使用 Comparable Type 的类型。所有的基本类型都是 Comparable 的，除此之外还有一些复合类型也是 Comparable Type，各种类型的比较规则在[go 语言规范中](https://golang.org/ref/spec)有详细的说明。

在文件`$GCROOT/compile/internal/type2/predicates.go`中定义了各种判断类型属性的方法，其中就包括判断给定类型是否是可排序的，或者是否是可比较的，代码如下：

```go
func isOrdered(typ Type) bool { return is(typ, IsOrdered) }
func Comparable(T Type) bool  { return comparable(T, nil) } // 
func comparable(T Type, seen map[Type]bool) bool {
	if seen[T] {
		return true
	}
	if seen == nil {
		seen = make(map[Type]bool)
	}
	seen[T] = true

	// If T is a type parameter not constrained by any type
	// list (i.e., it's underlying type is the top type),
	// T is comparable if it has the == method. Otherwise,
	// the underlying type "wins". For instance
	//
	//     interface{ comparable; type []byte }
	//
	// is not comparable because []byte is not comparable.
	if t := asTypeParam(T); t != nil && optype(t) == theTop {
		return t.Bound().IsComparable()
	}

	switch t := optype(T).(type) {
	case *Basic:
		// assume invalid types to be comparable
		// to avoid follow-up errors
		return t.kind != UntypedNil
	case *Pointer, *Interface, *Chan:
		return true
	case *Struct:
		for _, f := range t.fields {
			if !comparable(f.typ, seen) {
				return false
			}
		}
		return true
	case *Array:
		return comparable(t.elem, seen)
	case *Sum:
		pred := func(t Type) bool {
			return comparable(t, seen)
		}
		return t.is(pred)
	case *TypeParam:
		return t.Bound().IsComparable()
	}
	return false
}
```

可以发现 Function 类型不在 comparable 函数中的任何一个 case 中，因此它是不可比较的，而 struct 类型可比较的前提是其所有属性都是可比较类型。来看如下代码：

```go
func typecheck() {
	type User struct {
		Name string
	}

	type People struct {
		Name string
	}

	var u1, u2 User

	// case 1: 相同类型，且类型可比较，无编译错误
	if u1 == u2 {
		// No compile error
	}

	// case 2: 不同类型，虽然两个类型都是可比较类型，但由于类型不同，相互之间无法比较
	var p People
	if u1 == p {
		// Compile error: cannot compare u1 == p (mismatched types User and People) [MismatchedTypes]
	}

	// case 3: 相同类型，但函数类型不可比较，所以变量之间不可比较
	var f1 func()
	var f2 func()

	if f1 == f2 {
		// Compile error: cannot compare f1 == f2 (operator == not defined for func())
	}
}
```

上例中 User 类型所有的属性都是可比较的，所以 u1 == u2 没有问题，而 u1 与 p 是不同的类型，所以无法比较。再看如下代码：

```go
type User struct {
	Name string
	Age  func() int
}

var u1, u2 User

if u1 == u2 {
	// Compile error: cannot compare u1 == u2 (operator == not defuned for User) [UndefinedOp]
}
```

当我们给 User 添加一个函数字段时，类型检查器报告了编译错误，因为通过上面 comparable 方法可以发现，函数属性的加入让 User 变成不可比较的类型了，所以类型检查器会说`==` 操作符对该类型没有定义。


# 4.4.4-14 总结

至此，我们已经了解了编译器内部所有的类型数据结构，类型检查器的目的就是推导源代码中的表达式、变量、常量等程序组件的类型信息，即找出对应的上述结构，然后判断类型相互之间是否相互兼容。

接下来我们来看一下与类型检查器相关的数据结构。


# 4.4.5 类型检查器

与类型检查器相关的重要数据结构有三个，本节将分析其代码

## 4.4.5.1. Config

`Config`用来配置类型检查器，定义在`$GCROOT/compile/internal/types2/api.go`中：

```go
type Config struct {
	// GoVersion describes the accepted Go language version. The string
	// must follow the format "go%d.%d" (e.g. "go1.12") or ist must be
	// empty; an empty string indicates the latest language version.
	// If the format is invalid, invoking the type checker will cause a
	// panic.
	GoVersion string

	// If IgnoreFuncBodies is set, function bodies are not
	// type-checked.
	IgnoreFuncBodies bool

	// If FakeImportC is set, `import "C"` (for packages requiring Cgo)
	// declares an empty "C" package and errors are omitted for qualified
	// identifiers referring to package C (which won't find an object).
	// This feature is intended for the standard library cmd/api tool.
	//
	// Caution: Effects may be unpredictable due to follow-on errors.
	//          Do not use casually!
	FakeImportC bool

	// If IgnoreLabels is set, correct label use is not checked.
	// TODO(gri) Consolidate label checking and remove this flag.
	IgnoreLabels bool

	// If CompilerErrorMessages is set, errors are reported using
	// cmd/compile error strings to match $GOROOT/test errors.
	// TODO(gri) Consolidate error messages and remove this flag.
	CompilerErrorMessages bool

	// If go115UsesCgo is set, the type checker expects the
	// _cgo_gotypes.go file generated by running cmd/cgo to be
	// provided as a package source file. Qualified identifiers
	// referring to package C will be resolved to cgo-provided
	// declarations within _cgo_gotypes.go.
	//
	// It is an error to set both FakeImportC and go115UsesCgo.
	go115UsesCgo bool

	// If Trace is set, a debug trace is printed to stdout.
	Trace bool

	// If Error != nil, it is called with each error found
	// during type checking; err has dynamic type Error.
	// Secondary errors (for instance, to enumerate all types
	// involved in an invalid recursive type declaration) have
	// error strings that start with a '\t' character.
	// If Error == nil, type-checking stops with the first
	// error found.
	Error func(err error)

	// An importer is used to import packages referred to from
	// import declarations.
	// If the installed importer implements ImporterFrom, the type
	// checker calls ImportFrom instead of Import.
	// The type checker reports an error if an importer is needed
	// but none was installed.
	Importer Importer

	// If Sizes != nil, it provides the sizing functions for package unsafe.
	// Otherwise SizesFor("gc", "amd64") is used instead.
	Sizes Sizes

	// If DisableUnusedImportCheck is set, packages are not checked
	// for unused imports.
	DisableUnusedImportCheck bool
}
```

其中各个字段的注释写得非常详细，基本都是一些开关属性。比较重要的是`Importer`以及`Sizes`, 前者是[包加载器](/golang-bian-yi-qi-lei-xing-jian-cha/lei-xing-jian-cha-luo-ji)，而后者负责处理各类型的对齐问题。

## 4.4.5.2. Info

`Info`用来存放类型检查器的检查结果，定义在`$GCROOT/compile/internal/types2/api.go`中：

```go
// Info holds result type information for a type-checked package.
// Only the information for which a map is provided is collected.
// If the package has type errors, the collected information may
// be incomplete.
type Info struct {
	// Types maps expressions to their types, and for constant
	// expressions, also their values. Invalid expressions are
	// omitted.
	//
	// For (possibly parenthesized) identifiers denoting built-in
	// functions, the recorded signatures are call-site specific:
	// if the call result is not a constant, the recorded type is
	// an argument-specific signature. Otherwise, the recorded type
	// is invalid.
	//
	// The Types map does not record the type of every identifier,
	// only those that appear where an arbitrary expression is
	// permitted. For instance, the identifier f in a selector
	// expression x.f is found only in the Selections map, the
	// identifier z in a variable declaration 'var z int' is found
	// only in the Defs map, and identifiers denoting packages in
	// qualified identifiers are collected in the Uses map.
	Types map[syntax.Expr]TypeAndValue

	// Inferred maps calls of parameterized functions that use
	// type inference to the inferred type arguments and signature
	// of the function called. The recorded "call" expression may be
	// an *ast.CallExpr (as in f(x)), or an *ast.IndexExpr (s in f[T]).
	Inferred map[syntax.Expr]Inferred

	// Defs maps identifiers to the objects they define (including
	// package names, dots "." of dot-imports, and blank "_" identifiers).
	// For identifiers that do not denote objects (e.g., the package name
	// in package clauses, or symbolic variables t in t := x.(type) of
	// type switch headers), the corresponding objects are nil.
	//
	// For an embedded field, Defs returns the field *Var it defines.
	//
	// Invariant: Defs[id] == nil || Defs[id].Pos() == id.Pos()
	Defs map[*syntax.Name]Object

	// Uses maps identifiers to the objects they denote.
	//
	// For an embedded field, Uses returns the *TypeName it denotes.
	//
	// Invariant: Uses[id].Pos() != id.Pos()
	Uses map[*syntax.Name]Object

	// Implicits maps nodes to their implicitly declared objects, if any.
	// The following node and object types may appear:
	//
	//     node               declared object
	//
	//     *syntax.ImportDecl    *PkgName for imports without renames
	//     *syntax.CaseClause    type-specific *Var for each type switch case clause (incl. default)
	//     *syntax.Field         anonymous parameter *Var (incl. unnamed results)
	//
	Implicits map[syntax.Node]Object

	// Selections maps selector expressions (excluding qualified identifiers)
	// to their corresponding selections.
	Selections map[*syntax.SelectorExpr]*Selection

	// Scopes maps syntax.Nodes to the scopes they define. Package scopes are not
	// associated with a specific node but with all files belonging to a package.
	// Thus, the package scope can be found in the type-checked Package object.
	// Scopes nest, with the Universe scope being the outermost scope, enclosing
	// the package scope, which contains (one or more) files scopes, which enclose
	// function scopes which in turn enclose statement and function literal scopes.
	// Note that even though package-level functions are declared in the package
	// scope, the function scopes are embedded in the file scope of the file
	// containing the function declaration.
	//
	// The following node types may appear in Scopes:
	//
	//     *syntax.File
	//     *syntax.FuncType
	//     *syntax.BlockStmt
	//     *syntax.IfStmt
	//     *syntax.SwitchStmt
	//     *syntax.CaseClause
	//     *syntax.CommClause
	//     *syntax.ForStmt
	//
	Scopes map[syntax.Node]*Scope

	// InitOrder is the list of package-level initializers in the order in which
	// they must be executed. Initializers referring to variables related by an
	// initialization dependency appear in topological order, the others appear
	// in source order. Variables without an initialization expression do not
	// appear in this list.
	InitOrder []*Initializer
}
```

`Initializer`表示的是当前 package 的初始化顺序，其余的所有属性都是一个以 AST 节点为 key 的 map, 类型检查器会在类型检查的过程中收集对应的对象，Info 包含了所有类型检查的结果，其会在后续生成 IR Tree 时使用。其中涉及到的`TypeAndValue`以及`Inferred`都在本文件中定义。

## 4.4.5.3. Checker

类型检查的整体逻辑由`Checker`驱动，其定义在文件`$GCROOT/compile/internal/types2/check.go`中：

```go
type Checker struct {
	conf    *Config
	pkg     *Package                    // 当前正在编译的包
	*Info                               // 保存类型检查的结果
	version version                     // 支持的语言版本，例如 go1.16
	nextId  uint64                      // 泛型的结构 TypeParam 包含唯一 id, 通过该字段来自增生成
	objMap  map[Object]*declInfo        // maps package-level objects and (non-interface) methods to declaration info
	impMap  map[importKey]*Package      // maps (import path, source directory) to (complete or fake) package
	posMap  map[*Interface][]syntax.Pos // maps interface types to lists of embedded interface positions
	typMap  map[string]*Named           // maps an instantiated named type hash to a *Named type
	pkgCnt  map[string]int              // counts number of imported packages with a given name (for better error messages)

	// information collected during type-checking of a set of package files
	// (initialized by Files, valid only for the duration of check.Files;
	// maps and lists are allocated on demand)
	files        []*syntax.File            // list of package files
	imports      []*PkgName                // list of imported packages
	dotImportMap map[dotImportKey]*PkgName // maps dot-imported objects to the package they were dot-imported through

	firstErr error                    // first error encountered
	methods  map[*TypeName][]*Func    // 保存类型到其方法的映射
	untyped  map[syntax.Expr]exprInfo // map of expressions without final type
	delayed  []func()                 // stack of delayed action segments; segments are processed in FIFO order
	finals   []func()                 // list of final actions; processed at the end of type-checking the current set of files
	objPath  []Object                 // path of object dependencies during type inference (for cycle reporting)

	// context within which the current object is type-checked
	// (valid only for the duration of type-checking a specific object)
	context // 用来存放当前正在进行类型检查对象的上下文

	// debugging
	indent int // indentation for tracing
}
```

`Checker`定义了非常多的方法，几乎包含了所有的类型检查逻辑，而其属性封装了类型检查需要的各种数据结构，包括用于存放各种中间状态的属性，其中有几个属性还需要单独介绍一下：

* declInfo&#x20;

该结构用来封装 package-level 的顶级申明，与 AST 中的“申明（Declaration）”对应。其定义在文件`$GCROOT/compile/internal/types2/resolver.go`中：

```go
type declInfo struct {
    file      *Scope           // scope of file containing this declaration
    lhs       []*Var           // lhs of n:1 variable declarations, or nil
    vtyp      syntax.Expr      // type, or nil (for const and var declarations only)
    init      syntax.Expr      // init/orig expression, or nil (for const and var declarations only)
    inherited bool             // if set, the init expression is inherited from a previous constant declaration
    tdecl     *syntax.TypeDecl // type declaration, or nil
    fdecl     *syntax.FuncDecl // func declaration, or nil

    // 用来保存当前 declInfo 所依赖的对象，用来构建初始化依赖图
    deps map[Object]bool // lazily initialized
}
```

其内部封装了一个申明的各个语法树节点，类型检查主要围绕这个结构体展开。其中`deps`属性用来保存当前 declInfo 所依赖的对象，用于[构建初始化顺序](/golang-bian-yi-qi-lei-xing-jian-cha/4.5.31.4-gou-jian-chu-shi-hua-shun-xu)。

* delayed & finals

类型检查是分阶段进行的，有些模块的工作需要依赖其他部分的检查结果，这两个属性用来保存该类工作，所以其是两个函数列表。该属性在[类型检查逻辑](/golang-bian-yi-qi-lei-xing-jian-cha/4.5.31.3e-chu-li-delayed-dui-lie)中会有使用。


# 4.4.6 总结

类型检查器是编译器前端的一个非常复杂的模块，其中涉及到的数据结构非常多，我们这里只是列举出重要的几类数据结构进行分析，通过这几类数据结构，我们基本能够搭建起整个类型检查的骨架，因为整个类型检查都是围绕着这几个数据结构展开的。

总体来说，类型检查会对 AST 进行遍历并对每个节点进行处理，最终的结果是为每个 AST 的表达式创建出对应的类型数据结构，并将这些结果保存在 Info 中。


# 4.5.1 类型检查逻辑 - 包加载器

go 编译器一次编译的最大单元是一个包，输出的结果叫着对象文件（Object File），go 定义了自己的对象文件格式，并以`.a`或者`.o`为后缀存在于文件系统中。在`$GOROOT/pkg/$OS_$ARCH/`目录下就包含了所有标准库的对象文件，例如 linux 系统该目录为：`$GOROOT/pkg/linux_amd64`.

加载器在编译阶段解决包依赖问题，其任务就是通过包名找到对应的对象文件，然后将其解析成为上文提到的[Package](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.2-shu-ju-jie-gou-package)对象。加载器可以有各种实现，其接口定义在文件`$GCROOT/compile/internal/types2/api.go`中：

```go
type Importer interface {
	Import(path string) (*Package, error)
}

type ImportMode int

type ImporterFrom interface {
	Importer
	ImportFrom(path, dir string, mode ImportMode) (*Package, error)
}
```

两个接口的区别是方法`ImporterFrom`通过指定目标目录的方式来支持[vendor 机制](https://go.googlesource.com/proposal/+/master/design/25719-go15vendor.md), 而`Importer`无法支持，保留`Importer`接口是为了兼容性考虑，ImportMode 当前没有用途。加载器的实现在`$GCROOT/compile/internal/noder/import.go`中，抛开具体细节，其代码框架为：

```go
type gcimports struct {
	packages map[string]*types2.Package
}

func (m *gcimports) Import(path string) (*types2.Package, error) {
	return m.ImportFrom(path, "" /* no vendoring */, 0)
}

func (m *gcimports) ImportFrom(path, srcDir string, mode types2.ImportMode) (*types2.Package, error) { /* 忽略方法体 */
}
```

可见`Import`只是简单地调用`ImportFrom`方法，而后者负责整个加载解析逻辑。加载器配置在[Config](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.5-lei-xing-jian-cha-qi#1-config)中，并在类型检查的入口函数`check2()`中进行初始化，该函数在文件`$GCROOT/compile/internal/noder/irgen.go`中。

{% hint style="info" %}
go 编译过程的最后一件事情就是将编译结果导出成对象文件，所以包加载器只是该过程的一个逆操作，当前加载器的实现也是 Go 编译器团队在重写类型检查器的过程中临时的一个实现，后续可能会修改。

对于对象文件的详细格式、导出逻辑与加载逻辑我们没有必要深挖细刨，只需要掌握如下要点即可：&#x20;

1. 对象文件是二进制格式文件，是编译器的输出，链接器的输入&#x20;
2. 对象文件分为不同的块（Section）来组织数据，例如数据块，代码块，调试信息块等&#x20;
3. 对象文件内包含重定位（Relocation）信息，链接器在将多个对象链接在一起时，主要工作就是为需要重定位的数据或者指令规划内存地址&#x20;
4. go 是静态链接语言，在生成可执行文件时，链接器会将所有依赖的包链接在一起，形成一个独立的可执行文件&#x20;
5. 可以通过工具`go tool objdump`来查看对象文件或者可执行文件的内容
   {% endhint %}


# 4.5.2 类型检查逻辑 - 初始化

前文介绍了大量与类型检查相关的数据结构，这一节我们从如下两个方介绍类型检查器的初始化流程：

1. 初始化全局作用域
2. 初始化类型检查器

完成初始化之后，类型检查器才能开始正式进行类型检查工作。


# 4.5.2-1 全局作用域

简单地说，该部分的任务就是将语言的[内置符号](/golang-bian-yi-qi-lei-xing-jian-cha/4.3-fu-hao-jie-xi)初始化，并注册到[全局作用域](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.1-shu-ju-jie-gou-zuo-yong-yu)中。初始化逻辑在`$GCROOT/compile/internal/types2/universe.go`中，通过`init`函数在编译器启动时自动初始化：

```go
func init() {
	Universe = NewScope(nil, nopos, nopos, "universe") // 实例化全局作用域
	Unsafe = NewPackage("unsafe", "unsafe")
	Unsafe.complete = true

	defPredeclaredTypes()
	defPredeclaredConsts()
	defPredeclaredNil()
	defPredeclaredFuncs()
	defPredeclaredComparable()

	universeIota = Universe.Lookup("iota").(*Const)
	universeByte = Universe.Lookup("byte").(*TypeName).typ.(*Basic)
	universeRune = Universe.Lookup("rune").(*TypeName).typ.(*Basic)
	universeAny = Universe.Lookup("any").(*TypeName).typ.(*Interface)
	universeError = Universe.Lookup("error").(*TypeName).typ.(*Named)

	// "any" is only visible as constraint in a type parameter list
	delete(Universe.elems, "any")
}
```

该函数很清晰地表达了整个初始化过程，我们知道`Scope`中存放的是`Object`对象，所以各初始化函数的任务就是创建不同类型的`Object`对象并注册到全局变量`Universe`中。我们简单描述一下各个函数的任务：

* defPredeclaredTypes\
  &#x20;注册基本类型，例如 int8, float32, bool 等，包括前文提到的 Untyped 类型，该函数会为每种类型创建一个`TypeName`类型的 Object 对象。\
  \
  值得注意的是该方法还初始化了 error 类型，初始化代码如下：

```go
{
    res := NewVar(nopos, nil, "", Typ[String])                                // 方法返回类型为 string
    sig := &Signature{results: NewTuple(res)}                                 // 函数类型，只有返回 Tuple, 没有参数 Tuple
    err := NewFunc(nopos, nil, "Error", sig)                                  // 函数对象，函数名为 Error
    typ := &Named{underlying: NewInterfaceType([]*Func{err}, nil).Complete()} // 创建一个 defined type, 其 underlying type 是接口类型
    sig.recv = NewVar(nopos, nil, "", typ)                                    // 设置函数的 receiver, 使其成为上面接口的方法
    def(NewTypeName(nopos, nil, "error", typ))                                // 注册函数类型
}
```

&#x20;       上述代码相当于往 Universe 中注册了如下的接口类型：

```go
type error interface {
    Error() string
}
```

&#x20;       而这正是 go 中对 error 类型的要求：实现`Error() string`方法，所以 error 类型的接口并没有在任何地方进行显示地申明，而是在此处通过代码创建的。

* defPredeclaredConsts\
  &#x20;注册语言内置的常量，一共有三个：true, false, iota. 为每个常量创建一个`Const`类型的 Object 对象。
* defPredeclaredNil\
  注册`nil`, 其对应的是`Nil`类型的 Object 对象。
* defPredeclaredFuncs \
  注册内置函数，例如 append, new, panic 等，每个内置函数对应一个`Builtin`类型的 Object 对象。
* defPredeclaredComparable\
  与注册 error 类型相似，该方法注册了另一个隐式接口 comprable, 该接口是[泛型的预定义 constraint](https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#comparable-types-in-constraints). 例如可以如下方式使用该接口：

```go
// 使用 comparable 来限定 T 的类型
func compare[T comparable](i, j T) bool {} 
```


# 4.5.2-2 类型检查器

该部分对前文中关于[类型检查器](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.5-lei-xing-jian-cha-qi)的几个数据结构以及包加载器进行初始化，代码在`$GCROOT/compile/internal/noder/irgen.go`的`check2`中：

```go
// check2 type checks a Go package using types2, and then generates IR
// using the results.
func check2(noders []*noder) {
	if base.SyntaxErrors() != 0 {
		base.ErrorExit()
	}

	// setup and syntax error reporting
	var m posMap
	files := make([]*syntax.File, len(noders))
	for i, p := range noders {
		m.join(&p.posMap)
		files[i] = p.file
	}

	// 初始化 Config
	conf := types2.Config{
		GoVersion:             base.Flag.Lang,
		IgnoreLabels:          true, // parser already checked via syntax.CheckBranches mode
		CompilerErrorMessages: true, // use error strings matching existing compiler errors
		Error: func(err error) {
			terr := err.(types2.Error)
			base.ErrorfAt(m.makeXPos(terr.Pos), "%s", terr.Msg)
		},
		Importer: &gcimports{ // 初始化包加载器
			packages: make(map[string]*types2.Package),
		},
		Sizes: &gcSizes{},
	}
	// 初始化 Info, Info 是用来保存所有类型检查结果的容器，且只有当对应属性不为 nil 时，才保存对应的内容。这里将所有属性都初始化了，所以我们需要记录下所有检查结果
	info := types2.Info{
		Types:      make(map[syntax.Expr]types2.TypeAndValue),
		Defs:       make(map[*syntax.Name]types2.Object),
		Uses:       make(map[*syntax.Name]types2.Object),
		Selections: make(map[*syntax.SelectorExpr]*types2.Selection),
		Implicits:  make(map[syntax.Node]types2.Object),
		Scopes:     make(map[syntax.Node]*types2.Scope),
		Inferred:   make(map[syntax.Expr]types2.Inferred),
		// expand as needed
	}
	// 开始类型检查
	pkg, err := conf.Check(base.Ctxt.Pkgpath, files, &info)
	files = nil // 释放内存

	// 如果类型检查发生错误，则编译器退出
	base.ExitIfErrors()
	if err != nil {
		base.FatalfAt(src.NoXPos, "conf.Check error: %v", err)
	}
	if base.Flag.G < 2 { // 当前编译参数 -G, 如果当前想要完成后续操作，至少需要传入 -G=2
		os.Exit(0)
	}

	// 如果类型检查成功通过，则根据其结果生成 IR(Intermediate Representation), 后续章节会专门介绍
	g := irgen{
		target: typecheck.Target,
		self:   pkg,
		info:   &info, // info 包含了类型检查的结果，继续传递给 irgen 使用
		posMap: m,
		objs:   make(map[types2.Object]*ir.Name),
		typs:   make(map[types2.Type]*types.Type),
	}
	g.generate(noders)

	if base.Flag.G < 3 {
		os.Exit(0)
	}
}
```

对`Checker`的初始化在文件`$GCROOT/compile/internal/types2/api.go`的`Check`方法内：

```go
func (conf *Config) Check(path string, files []*syntax.File, info *Info) (*Package, error) {
	pkg := NewPackage(path, "")
	return pkg, NewChecker(conf, pkg, info).Files(files)
}
```

该方法是类型检查的入口，从这里开始，编译器完成类型检查器的初始化，正式开始工作。


# 4.5.3 类型检查逻辑 - 流程分析

完成初始化之后，类型检查器开始正式工作，类型检查的逻辑非常复杂，涉及到很多琐碎的细节处理，并且类型检查器的开发工作也在一直进行当中，所以逐行地对当前逻辑进行过度细致的讲解并没有太大的意义，本节我们将采用如下方式进行分析：&#x20;

1. 分析类型检查的主要步骤&#x20;
2. 罗列重要的函数、方法的位置 ，以及功能
3. 针对性的示例分析

期望能够帮助读者达成如下效果：&#x20;

1. 了解类型检查器工作原理与设计思路&#x20;
2. 熟悉当前代码结构，以及重要逻辑的函数位置&#x20;
3. 熟练地通过 UT 对现有代码进行测试，以便校验自己的想法

总而言之，本节的目的不是讲解代码，而是帮助读者熟悉代码框架，以便在读者对 go 的类型系统有任何疑问时，都可以自己动手在源码中找到答案。


# 4.5.3-1.1 总体流程

类型检查的总体流程定义在`$GCROOT/compile/internal/types2/check.go`文件的方法`checkFiles`中，主体代码如下：

```go
func (check *Checker) checkFiles(files []*syntax.File) (err error) {
	check.initFiles(files)
	check.collectObjects()
	check.packageObjects()
	check.processDelayed(0) // incl. all functions
	check.initOrder()
	if !check.conf.DisableUnusedImportCheck {
		check.unusedImports()
	}
	check.recordUntyped()
	if check.Info != nil {
		sanitizeInfo(check.Info)
	}
	return
}
```

这里只保留了体现类型检查步骤的代码，删除的部分并不会影响我们对整个流程的理解。这几个方法包含了类型检查的所有重要内容，概括起来可以分为三个部分：&#x20;

1. 类型检查准备工作 \
   check.initFiles(files) 与 check.collectObjects()&#x20;
2. 类型检查 \
   check.packageObjects() 与 check.processDelayed(0)&#x20;
3. 确定全局变量初始化顺序 \
   check.initOrder()

我们接下来对各个部分进行分析。


# 4.5.3-1.2 类型检查准备工作

类型检查的准备工作涉及到两个方法：`check.initFiles()`与`check.collectObjects()`.

编译器一次编译的文件只能属于同一个包，而方法`checkFiles` 的输入是一个 AST 列表，`check.initFiles()`会检查所有 AST 是否属于同一个包，并初始化字段 check.files 属性。

在分析方法`check.collectObjects()`之前，我们再来回顾一下类型检查的总体概念以及与之相关的数据结构。程序中涉及到类型的地方有如下几个方面：&#x20;

1. 类型的申明，即创建新的类型&#x20;
2. 类型的使用，例如申明变量时，或者申明函数的参数与返回值时需要指明类型信息&#x20;
3. 类型的推导，即推导出表达式返回值的类型，例如表达式`1 + 1.0`的类型是浮点数
4. &#x20;类型的兼容性检查，检查在代码中实际类型与期望类型是否一致，例如判断一个类型的值能够赋给另一个类型，或者两个类型是否等价、是否可以相互转换

程序的类型信息通过[类型数据结构](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.41-lei-xing-shu-ju-jie-gou-jian-jie)来表示，类型检查的目的就是为所有需要类型信息的地方创建对应的类型结构，并判断相互之间是否兼容。Go 语言要求每个标识符都必须先申明再使用，我们可以将标识符的申明看着在程序中创建了一个新的对象，并给该对象取了一个名字，即一个命名对象（Named Entity），一个命名对象用一个[Object对象](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.3-shu-ju-jie-gou-object-dui-xiang)来表示。类型检查说到底需要对源代码进行分析，即语法分析的输出：AST ，但仔细观察[Object 接口](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.3-shu-ju-jie-gou-object-dui-xiang#1-jie-kou-ji-gong-yong-jie-gou)会发现该对象并没有封装 AST 中的节点，类型检查器将 Object 对象对应的 AST 节点信息封装在[declInfo](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.5-lei-xing-jian-cha-qi#3-checker)中，而 Object 对象与 declInfo 的对应关系保存在`checker.objMap`中。

现在回到`check.collectObjects()`上来，在[Object的作用](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.3-shu-ju-jie-gou-object-dui-xiang#4-object-de-zuo-yong)中我们提到类型检查是围绕着 Object 对象展开的，类型检查器需要为每个 (Object, declInfo) 组合构造出对应的[类型数据结构](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.41-lei-xing-shu-ju-jie-gou-jian-jie)。而方法`check.collectObjects()`的任务就是通过 AST 创建出 Object 与 declInfo；除此之外，该方法还会检查包内的命名冲突，以及将所有的方法与其 Receiver 进行绑定。该方法定义在文件`GCROOT/compile/internal/types2/resolver.go`中，其代码的总体框架如下：

```go
func (check *Checker) collectObjects() {
	type methodInfo struct {
		obj  *Func        // method
		ptr  bool         // true if pointer receiver
		recv *syntax.Name // receiver type name
	}
	var methods []methodInfo // collected methods with valid receivers and non-blank _ names
	var fileScopes []*Scope

	// 1. 处理 AST 中的顶级申明，创建 Object 与 declInfo 对象
	for fileNo, file := range check.files {
		// 创建文件作用域
		fileScope := NewScope(check.pkg.scope, startPos(file), endPos(file), check.filename(fileNo))
		fileScopes = append(fileScopes, fileScope)
		check.recordScope(file, fileScope)

		for index, decl := range file.DeclList {
			switch s := decl.(type) {
			case *syntax.ImportDecl:
				// 通过包加载器将引用的包加载进来
				imp := check.importPackage(s.Path.Pos(), path, fileDir)

				pkgName := NewPkgName(s.Pos(), pkg, name, imp)
				if s.LocalPkgName != nil {
					// 如果是 import db "runtime/debug", 则将 LocalPkgName, 即这里的包别名 db, 注册到 info.Defs 中
					check.recordDef(s.LocalPkgName, pkgName)
				} else {
					check.recordImplicit(s, pkgName)
				}
				check.imports = append(check.imports, pkgName)
				if name == "." {
					// 如果是  import . "xxxxx", 则将包 xxxxx 中的 public 符号导入当前文件作用域
					if check.dotImportMap == nil {
						check.dotImportMap = make(map[dotImportKey]*PkgName)
					}
					for _, obj := range imp.scope.elems {
						if obj.Exported() {
							fileScope.Insert(obj)
							check.dotImportMap[dotImportKey{fileScope, obj}] = pkgName
						}
					}
				} else {
					// 将包对象插入当前的文件作用域
					check.declare(fileScope, nil, pkgName, nopos)
				}
			case *syntax.ConstDecl:
				// 得到常量的初始化表达式列表
				values := unpackExpr(last.Values)
				for i, name := range s.NameList {
					// 创建常量的 Object 对象
					obj := NewConst(name.Pos(), pkg, name.Value, nil, iota) // iota 表示当前 ConstDecl 在当前常量组内的索引。常量组表示用括号括起来的一组常量申明
					var init syntax.Expr
					if i < len(values) {
						init = values[i]
					}
					// 创建常量对应的 declInfo 对象
					d := &declInfo{file: fileScope, vtyp: last.Type, init: init, inherited: inherited}
					// 将 obj 放入包作用域，并保存 obj -> d 的映射关系到 check.objMap 中
					check.declarePkgObj(name, obj, d)
				}

				// arity 方法来用判断常量名称（s.NameList）与初始化表达式（values）的个数是否相等
				check.arity(s.Pos(), s.NameList, values, true, inherited)

			case *syntax.VarDecl:
				// lhs - left hand side, 用来对应变量申明中左边的变量名列表，这里会为每个变量创建一个 Object 对象。
				// 与变量申明不同的是，变量的初始化表达式的个数有两种情况：
				// 1. 数目与 lhs 变量名个数一样。此时每个变量的 Object 对象都对应自己的初始化表达式的 declInfo 对象
				// 2. 一个表达式，返回值的个数与 lhs 变量名个数一样。此时所有的 Object 对象都复用同一个 declInfo 对象
				lhs := make([]*Var, len(s.NameList))

				// 所以如果初始化表达式 s.Values 不是一个列表的话，则创建一个所有变量 Object 复用的 declInfo 对象
				var d1 *declInfo
				if _, ok := s.Values.(*syntax.ListExpr); !ok {
					d1 = &declInfo{file: fileScope, lhs: lhs, vtyp: s.Type, init: s.Values}
				}

				values := unpackExpr(s.Values) // 尝试着将初始化表达式转换为列表
				for i, name := range s.NameList {
					// 为每个变量创建 Object 对象
					obj := NewVar(name.Pos(), pkg, name.Value, nil)
					lhs[i] = obj

					d := d1
					if d == nil {
						// 如果没有复用的 declInfo 对象，则从初始化列表中取出对应的表达式，并创建 declInfo 对象
						var init syntax.Expr
						if i < len(values) {
							init = values[i]
						}
						d = &declInfo{file: fileScope, vtyp: s.Type, init: init}
					}

					// 将 obj 放入包作用域，并保存 obj -> d 的映射关系到 check.objMap 中
					check.declarePkgObj(name, obj, d)
				}

				// 如果变量类型为空的话，则必定包含初始化表达式，此时需要校验变量名与初始化表达式的个数是否合理
				if s.Type == nil || values != nil {
					check.arity(s.Pos(), s.NameList, values, false, false)
				}

			case *syntax.TypeDecl:
				// 对于类型申明，直接创建 TypeName 对象与 declInfo 对象
				obj := NewTypeName(s.Name.Pos(), pkg, s.Name.Value, nil)
				check.declarePkgObj(s.Name, obj, &declInfo{file: fileScope, tdecl: s})

			case *syntax.FuncDecl:
				d := s
				name := d.Name.Value
				obj := NewFunc(d.Name.Pos(), pkg, name, nil) // 创建函数的 Object 对象，函数与方法的申明都对应的是 FuncDecl, 接下来对二者分开处理
				if d.Recv == nil {
					if name == "init" {
						obj.parent = pkg.scope
						check.recordDef(d.Name, obj) // 如果是 init 方法，则只存入 info.Defs 中而不插入当前的包作用域内，因为 init 方法不能被其他方法调用
					} else {
						check.declare(pkg.scope, d.Name, obj, nopos) // 如果是普通方法，则需要插入当前的包作用域内，用于符号解析
					}
				} else {
					ptr, recv, _ := check.unpackRecv(d.Recv.Type, false)
					if recv != nil && name != "_" {
						methods = append(methods, methodInfo{obj, ptr, recv}) // 在本方法的最后需要将所有的方法与其 Receiver 绑定，所以此处记录一下方法对象
					}
					check.recordDef(d.Name, obj) // 方法的解析通过 Receiver 来完成，所以只将该对象放入 info.Defs 即可，不能插入包作用域内
				}
				info := &declInfo{file: fileScope, fdecl: d} // 创建函数对应的 declInfo
				check.objMap[obj] = info                     // 保存 obj -> declInfo 的映射关系
				obj.setOrder(uint32(len(check.objMap)))      // 用于对方法进行排序
			default:
				check.errorf(s, invalidAST+"unknown syntax.Decl node %T", s)
			}
		}
	}

	// 2. 检查包内的命名冲突
	for _, scope := range fileScopes {
		for _, obj := range scope.elems {
			if alt := pkg.scope.Lookup(obj.Name()); alt != nil {
				var err error_
				if pkg, ok := obj.(*PkgName); ok {
					err.errorf(alt, "%s already declared through import of %s", alt.Name(), pkg.Imported())
					err.recordAltDecl(pkg)
				} else {
					err.errorf(alt, "%s already declared through dot-import of %s", alt.Name(), obj.Pkg())
					// TODO(gri) dot-imported objects don't have a position; recordAltDecl won't print anything
					err.recordAltDecl(obj)
				}
				check.report(&err)
			}
		}
	}

	// 3. 将所有的方法与其 receiver 绑定起来
	if methods != nil {
		check.methods = make(map[*TypeName][]*Func)
		for i := range methods {
			m := &methods[i]
			ptr, base := check.resolveBaseTypeName(m.ptr, m.recv) // 解析 receiver 的基础类型
			if base != nil {
				m.obj.hasPtrRecv = ptr
				check.methods[base] = append(check.methods[base], m.obj)
			}
		}
	}
}
```

这里的代码只是为了展现核心的处理思路，为了节约篇幅，很多细节以及错误检查的部分都删除了。在三个步骤中，最重要的是第一部分：Object 对象与 declInfo 的创建与注册。其中涉及到的几个注册方法的概要逻辑如下：

```go
// id 不能为 nil, 将对象放入 info.Defs 中
func (check *Checker) recordDef(id *syntax.Name, obj Object) {
	if m := check.Defs; m != nil {
		m[id] = obj
	}
}

// 将 obj 插入 scope 中，如果 id 不为 nil, 则同时放入 info.Defs 中
func (check *Checker) declare(scope *Scope, id *syntax.Name, obj Object, pos syntax.Pos) {
	if obj.Name() != "_" {
		if alt := scope.Insert(obj); alt != nil {
			// 忽略错误处理代码
			return
		}
		obj.setScopePos(pos)
	}
	if id != nil {
		check.recordDef(id, obj)
	}
}

// 将 obj 注册到包作用域内，并建立 obj -> d 的映射关系
func (check *Checker) declarePkgObj(ident *syntax.Name, obj Object, d *declInfo) {
	// 忽略对象名称校验逻辑：对象名称不能为 init, main
	check.declare(check.pkg.scope, ident, obj, nopos)
	check.objMap[obj] = d
	obj.setOrder(uint32(len(check.objMap)))
}
```

总而言之，该方法会向下列字段添加内容：

* info.Defs
* info.Implicits
* checker.imports
* checker.objMap
* checker.methods

细心的读者可能已经发现：这里只是创建了顶级申明的类型检查对象，但是对于源代码中的每个表达式，或者涉及到类型使用的语句（例如赋值语句 a = foo()），类型检查都是需要的，那么这些地方的类型检查是如何完成的呢？答案是类型检查是通过递归下降的方式进行的，当类型检查器对顶级申明的对象进行类型检查时，其会递归地对当前 AST 节点的所有子节点进行类型检查，这里的逻辑相当于只是类型检查的初始推动力，完成了这里的工作之后，真正的类型检查就开始了。


# 4.5.3-1.3 类型检查核心逻辑

本节及后续相关章节包含类型检查的核心处理逻辑，我们将从如下几方面进行讲解：

1. 总体介绍
2. 类型表达式的类型检查
3. 求值表达式的类型检查
4. 类型兼容性检查
5. 处理 delayed 队列


# 4.5.3-1.3a 总体介绍

类型检查分为两个阶段，第一阶段由方法`check.packageObjects()`展开，通过递归下降的方式对 check.objMap 中的对象进行类型检查；第二阶段由方法`check.processDelayed()`处理，之所以需要该步骤是因为某些逻辑需要在全局申明的类型检查结束后才能进行，例如检查类型合法性（是否有环形依赖）、对函数体进行检查等。

`check.packageObjects()`是类型检查的入口，其定义在`$GCROOT/compile/internal/types2/resolver.go`中，其逻辑很简单：将 check.objMap 中的 Object 对象按照申明的顺序排序，然后先对非类型别名（通过 type A = B 这种形式申明的类型）的对象进行类型检查，再对类型别名的对象进行类型检查。其代码如下：

```go
func (check *Checker) packageObjects() {
	// 将 Object 对象按照申明的顺序排序
	objList := make([]Object, len(check.objMap))
	i := 0
	for obj := range check.objMap {
		objList[i] = obj
		i++
	}
	sort.Sort(inSourceOrder(objList))

	// 对于已经完成类型检查的对象，收集该对象的方法。因为 check.Files 可能被调用多次，所以第一次调用时该 for 循环不会有任何作用
	for _, obj := range objList {
		if obj, _ := obj.(*TypeName); obj != nil && obj.typ != nil {
			check.collectMethods(obj)
		}
	}

	var aliasList []*TypeName
	// 步骤一
	for _, obj := range objList {
		if tname, _ := obj.(*TypeName); tname != nil && check.objMap[tname].tdecl.Alias {
			// 如果是类型别名，则收集起来在步骤二中处理
			aliasList = append(aliasList, tname)
			continue
		}

		// 对非类型别名的对象进行类型检查
		check.objDecl(obj, nil)
	}
	// 步骤二：对类型别名进行类型检查
	for _, obj := range aliasList {
		check.objDecl(obj, nil)
	}

	check.methods = nil // 释放内存
}
```

对于每个 Object 对象都会调用方法`check.objDecl()`进行类型检查，该方法也是类型检查真正开始的地方。根据之前的分析，我们知道每个 Object 对象都与 AST 中的一个 Node 对应，所以从根本上来说， `check.objDecl()`将会对该 Node 的所有子节点进行类型检查，而在对子节点进行检查的过程中，可能又会递归调用到`check.objDecl()`，下图描述了该图的一个大概调用栈：

![类型检查函数调用关系](/files/-Mal7YpbixFFfjvTZEer)

check.objDecl() 会根据 Object 对象的不同类别调用对应的方法进行检查，其中右边方框表示的是类型检查的底层方法，这类方法非常多，基本上对于每类 AST 节点都有一个，这里只罗列了核心的几个，事实上整个`$GCROOT/compile/internal/types2/`目录下绝大多数逻辑都可以包含在该方框内。

接下来我们对核心逻辑的处理思路进行单独分析。


# 4.5.3-1.3b 类型表达式的类型检查

Go 在语法分析中使用表达式（Expr）表示类型值，例如对于如下类型申明：

```go
 type T struct {
     name string
 }
```

编译器会为`struct {}`这块代码创建表达式`syntax.StructType`. 而对于下列申明：

```go
 var pipe chan int
```

代表类型的`chan int`对应的是表达式`syntax.ChanType`.

对类型表达式进行类型检查的入口函数是`check.typ()`, 核心逻辑封装在方法`check.typInternal()`中，定义在文件`GCROOT/compile/internal/types2/typexpr.go`内，其代码结构如下：

```go
 func (check *Checker) typInternal(e0 syntax.Expr, def *Named) (T Type) {
     switch e := e0.(type) {
     case *syntax.BadExpr:
         // ignore - error reported before
     case *syntax.Name:
         var x operand
         check.ident(&x, e, def, true)
         switch x.mode {
         case typexpr:
             typ := x.typ
             def.setUnderlying(typ)
             return typ
         case invalid:
             // ignore - error reported before
         case novalue:
             check.errorf(&x, "%s used as type", &x)
         default:
             check.errorf(&x, "%s is not a type", &x)
         }
     case *syntax.SelectorExpr:
     case *syntax.IndexExpr:
     case *syntax.ParenExpr:
     case *syntax.ArrayType:
     case *syntax.SliceType:
     case *syntax.DotsType:
     case *syntax.StructType:
     case *syntax.Operation:
     case *syntax.FuncType:
     case *syntax.InterfaceType:
     case *syntax.MapType:
     case *syntax.ChanType:
         typ := new(Chan)
         def.setUnderlying(typ)

         dir := SendRecv
         switch e.Dir {
         case 0:
             // nothing to do
         case syntax.SendOnly:
             dir = SendOnly
         case syntax.RecvOnly:
             dir = RecvOnly
         default:
             check.errorf(e, invalidAST+"unknown channel direction %d", e.Dir)
             // ok to continue
         }

         typ.dir = dir
         typ.elem = check.varType(e.Elem)
         return typ

     default:
         check.errorf(e0, "%s is not a type", e0)
         check.use(e0)
     }

     typ := Typ[Invalid]
     def.setUnderlying(typ)
     return typ
 }
```

通过方法签名可知：该方法通过一个表达式推导出一个类型。其中每个 case 都对应一个类型表达式，这里只留下`syntax.Name`与`syntax.ChanType`进行演示。

`check.typInternal()`采用递归方式对表达式进行类型检查，例如对于`syntax.ChanType`, 最后调用了`check.varType()`对 channel 的元素类型进行检查，而后者最终又会回到`check.typInternal()`中来。递归算法都是基于[Divide-and-Conquer](https://en.wikipedia.org/wiki/Divide-and-conquer_algorithm)的思路来解决问题的，对于任何递归函数的分析，都需要牢牢把握两方面的信息：&#x20;

1. base-case 是什么？即何时遇到原子问题终止递归&#x20;
2. 问题如何分解？即一个大问题如何被拆分成更小的问题

对于`check.typInternal()`而言，问题分解就是上面方法中每个 case 的处理逻辑；而 base-case 对应的就是语言的基础类型，当类型检查遇到`int32`, `bool`, `string`等类型时，此时对应的是`syntax.Name`这个 case, 接下来编译器会通过`check.ident()`方法对该标识符（identifier）进行类型检查，而在[全局作用域](/golang-bian-yi-qi-lei-xing-jian-cha/4.5.21-quan-ju-zuo-yong-yu)的初始化过程中，编译器已经将所有基础类型注册好了，所以`check.ident()`方法会成功完成符号解析并拿到对应的类型结构，如果是`int32`, 则会得到一个`TypeName`的 Object 对象，其类型是`Basic{kind: Int32, info: IsInteger, name: "int32"}`.

如果类型表达式的所有子结构都没有问题，那么`check.typInternal()`最后就会返回对应的类型数据结构，从而成功完成类型检查，该函数是类型检查过程中编译器创建各种类型结构的核心。


# 4.5.3-1.3c 求值表达式的类型检查

求值表达式是指程序中进行求值运算的代码块，例如算术运算、函数调用、访问数组或对象属性、类型转换等。

{% hint style="info" %}
求值表达式并不是一个特别严格、准确的术语，这里使用只是为了区分类型表达式，从最抽象的层面上来看，程序可以分为控制语句与计算单元，控制语句通过关键字来实现，例如 for, if 等用来控制程序的执行流，而 type, func 等用来告诉编译器需要创建新的类型；计算单元则用来达成程序的业务目的，这里所说的求值表达式属于计算单元，但也并不是所有的计算单元都会产生一个明确的返回值，例如一个函数可能没有返回值，调用函数只是为了该函数的副作用（Side effect）。当然有的语言为了使文法更加规范，使用一个特殊的值（void）来表示“没有值”。
{% endhint %}

对于求值表达式，类型检查器除了进行类型检查之外，还会尝试着对其求值，例如表达式`"Hello, " + "Golang"`，运算符号`+`的两个运算符都是常量，所以在类型检查的过程中就会完成字符串的拼接操作，从而得到最终结果`"Hello, Golang"` 。类型检查器会尽可能得对表达式进行求值，以便提升程序在运行时的效率。

对求值表达式进行类型检查的入口是方法`check.expr()`, 核心逻辑封装在方法`check.exprInternal()`内，定义在文件`$GCROOT/compile/internal/types2/expr.go`中。 `check.exprInternal()`是一个 700 行代码的胖方法，但其基本的处理思路与`check.typInternal()`是一样的，都是通过 switch...case... 语句根据表达式的类型进行递归检查，我们贴出部分代码一探究竟：

```go
func (check *Checker) exprInternal(x *operand, e syntax.Expr, hint Type) exprKind {
	switch e := e.(type) {
	case *syntax.Name:
		// 对标识符进行类型检查，如果该标识符已经完成了类型检查，则递归终止，否则递归进行检查
		check.ident(x, e, nil, false)
	case *syntax.BasicLit:
		// 基本类型都是常量表达式，设置常量值。
		x.setConst(e.Kind, e.Value)
	case *syntax.FuncLit:
		// 函数字面量是形如 func() {} 的表达式，先对函数类型进行检查，再将函数体的检查推入 delayed 队列
		if sig, ok := check.typ(e.Type).(*Signature); ok {
			decl := check.decl
			iota := check.iota
			check.later(func() {
				check.funcBody(decl, "<function literal>", sig, e.Body, iota)
			})
			x.mode = value
			x.typ = sig
		}
	}
}
```

这里删除了绝大多数代码，只保留了三个 case 的部分代码来帮助我们理解该方法的处理思路。其中`syntax.Name`与`syntax.BasicLit`是该函数递归的 base-case, 而`syntax.FuncLit`则体现了对符合表达式的分解，并且可以发现对函数体的类型检查被推入了 delayed 队列，这在后续会有介绍。

对求值表达式的检查结果放在结构体`operand`中，其定义如下：

```go
type operand struct {
	mode operandMode
	expr syntax.Expr
	typ  Type
	val  constant.Value
	id   builtinId
}
```

该结构用来保存类型检查的中间值，属性`typ`与`val`分别用来保存推导出来的类型以及计算出来的值。


# 4.5.3-1.3d 类型兼容性检查

类型的期望类型与实际类型是否兼容，是类型兼容性检查的核心，例如赋值语句`var name string = getName();`中，name 的期望类型是 string, 因此函数 getName() 的返回值必须也是 string, 类型检查器需要对此进行核实。

程序很多地方都会涉及到类型兼容性问题，例如赋值、函数定义、channel 写入等等，该问题的核心到最后往往体现为“能否将一个值赋给指定类型”，判断的核心逻辑定义在文件`$GCROOT/compile/internal/types2/assignments.go`中，入口函数是`check.assignment()`, 抛开细节，其主体逻辑如下:

```go
// x 是赋值语句的右值，而 T 是赋值语句左边的目标类型，context 用来记录上下文信息，例如 "return statement", 便于提示错误信息
func (check *Checker) assignment(x *operand, T Type, context string) {
	if isUntyped(x.typ) {
		// 处理 x 是 UntypedXXX 的情况，尝试将 x.val 的值转换给类型 T
		check.convertUntyped(x, target)
	}
	if reason := ""; !x.assignableTo(check, T, &reason) {
		// 判断 x 能够赋值给类型 T, 此处删除错误处理信息
	}
}
```

`assignableTo()`方法定义在文件`$GCROOT/compile/internal/types2/operand.go`中，删掉细节代码，其主体逻辑如下：

```go
func (x *operand) assignableTo(check *Checker, T Type, reason *string) bool {
	V := x.typ
	if check.identical(V, T) { // x 的类型与目标类型等价，则可以赋值
		return true
	}

	Vu := optype(V) // 拿到运算符的 underlying type
	Tu := optype(T) // 拿到目标类型的 underlying type

	// 处理 x 是常量值的情况
	if isUntyped(Vu) {
		switch t := Tu.(type) {
		case *Basic:
			// 目标类型是基础类型，此处判断 x 是否能够表示为该基础类型
		case *Sum:
			// 如果 x 可以赋值给 Sum 类型所带表的所有类型，则返回 true
			return t.is(func(t Type) bool {
				return x.assignableTo(check, t, reason)
			})
		case *Interface:
			check.completeInterface(nopos, t)
			return x.isNil() || t.Empty() // 如果 T 是 interface, 此时因为 x 是基础类型。那么只有当 x 是 nil, 或者 T 是 interface{} 时，x 才可以赋值给 T
		case *Pointer, *Signature, *Slice, *Map, *Chan:
			return x.isNil() // nil 可以赋值给指针、函数、切片、Map 以及 chan 类型
		}
	}

	/*
	 * 如果两个类型的 underlying type 是等价的，那么只要两个类型不同时是 defined type, 就可以进行赋值
	 * 详见 underlying type 一节的讨论
	 */
	if check.identical(Vu, Tu) && (!isNamed(V) || !isNamed(T)) {
		return true
	}

	// 如果 T 是 interface, 则判断 x 是否实现了该接口
	if Ti, ok := Tu.(*Interface); ok {
		// 判断类型 V 是否实现接口 Ti 的方式是检查 V 的方法集合是否覆盖了 Ti 的所有方法
		if m, wrongType := check.missingMethod(V, Ti, true); m != nil /* Implements(V, Ti) */ {
			// 找到缺失方法，进行错误处理
			return false
		}
		return true
	}

	// 判断两个 channel 类型是否可以赋值
	if Vc, ok := Vu.(*Chan); ok && Vc.dir == SendRecv {
		if Tc, ok := Tu.(*Chan); ok && check.identical(Vc.elem, Tc.elem) {
			return !isNamed(V) || !isNamed(T)
		}
	}

	return false
}
```

这两个方法是兼容性检查的主体思路，`check.identical()`我们已经在[类型的等价规则](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.412-lei-xing-de-deng-jia-gui-ze-fl)中讨论过，UntypedXXX 的转换规则也已经在[基础类型的Untyped 部分](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.43-lei-xing-shu-ju-jie-gou-ji-chu-lei-xing)中讨论过，后续章节会详细讨论 Underlying Type 的赋值规则，以及如何判断一个类型是否实现了某个接口。


# 4.5.3-1.3e 处理delayed队列

这是类型检查的第二阶段，即执行 check.delayed 里面的函数。在进行第一阶段的类型检查时，很多事情无法在对表达式进行递归检查时完成，这些任务会被推入 check.delayed 中。典型的几类放入延迟处理队列的任务如下：

## 函数体的类型检查

函数体的类型检查必须在全局申明的类型检查之后进行，因为函数体与全局类型申明可能会相互引用，例如如下例子：

```go
type I interface {
  m([unsafe.Sizeof(func() { I.m(nil) })]byte)
}
```

{% hint style="info" %}
可以这样申明方法是因为 unsafe 包内的方法在编译阶段执行，而不是运行阶段
{% endhint %}

可以发现方法 I.m 参数中所涉及到的函数体又引用了 I.m. 如果在对 I.m 进行类型检查的过程中就对函数体（`I.m(nil)`）进行类型检查的话，由于此时`I.m`还没有完成检查，方法 m 还没有被收录到接口`I`中，所以在函数体中将无法成功地完成对符号`I.m`的解析，编译器会报错：I.m undefined (type I has no field or method m).

对函数体进行类型检查的函数是`check.funcBody()`, 查看其调用者（Caller）就可以看到被推入 delayed 队列中的任务。

## 循环依赖检查

编译器要做的一件重要事情便是计算一个类型所需要的空间大小，基于这一点，只要类型中出现了循环引用，那么就是不合法的，例如：

```go
type T T // 类型会无穷展开
type A struct {
    self A // 递归引用，类型同样会无穷展开
}
```

这种递归引用会导致编译器在计算空间时陷入死循环，所以编译器需要识别出这种情况并报错。在完成全局声明的类型检查之后，就需要对每个类型进行循环依赖检查，该逻辑通过`$GCROOT/compile/internal/types2/decl.go`中的函数`func (check *Checker) validType(typ Type, path []Object) typeInfo {}`来完成。类型依赖的拓扑结构本质上是一个有向图（Directed Graph），该函数通过深度优先遍历（Deep-First Search）的方式检测是否存在环（Circle）。其主体逻辑如下：

```go
func (check *Checker) validType(typ Type, path []Object) typeInfo {
    const (
        unknown typeInfo = iota
        marked
        valid
        invalid
    )

    switch t := typ.(type) {
    case *Array:
        return check.validType(t.elem, path)

    case *Struct:
        for _, f := range t.fields {
            if check.validType(f.typ, path) == invalid {
                return invalid
            }
        }

    case *Interface:
        for _, etyp := range t.embeddeds {
            if check.validType(etyp, path) == invalid {
                return invalid
            }
        }

    case *Named:
        switch t.info {
        case unknown:
            t.info = marked
            t.info = check.validType(t.orig, append(path, t.obj))
        case marked:
            for i, tn := range path {
                if tn == t.obj {
                    // 检测到循环依赖
                    check.cycleError(path[i:])
                    t.info = invalid
                    return t.info
                }
            }
            panic("internal error: cycle start not found")
        }
        return t.info

    case *instance:
        return check.validType(t.expand(), path)
    }

    return valid
}
```

其中参数`typ`是待检查类型，而`path`是当前已经遍历过的对象路径，只有`Named`这个 case （Defined Type) 可能产生循环依赖，如果当前对象已被标记并且在`path`中，则我们就发现了循环依赖。而对于其它复合类型，继续递归进行检查即可。

通过上述代码可以发现对于指针类型，总是不会有循环依赖（指针类型不在任何 case 中，方法总是返回 valid），例如如下类型申明就是合法的：

```go
type T *T
type A struct {
    self *A
}
```

这是因为指针所需要的内存空间是固定的，所以编译器可以顺利地计算出上面类型所需要的地址空间。

{% hint style="info" %}
`type T *T` 这种申明符合语法，但却并没有实际的应用场景。Go 语言允许这种申明存在是为了在设计上及可能保证语言的简洁性与正交性，因此并不会为了避免无用结构而刻意添加特殊的语言规则。
{% endhint %}

## 整理接口信息&#x20;

对于接口类型，在完成类型检查后还需要做一些信息整理工作，例如将所有内嵌接口的方法整合到一起，如果声明了泛型的 constraints 的话，还需要推算出该接口实际的 constraints 类型（在前文介绍`top` 与`bottom`类型时已有介绍），这些逻辑都在方法`check.completeInterface()`中完成。

## 其他情况

例如检查 map 类型的 key 的类型是否是 Comparable 的。还有各种其它情况需要放入延迟队列，这里不再一一列出。


# 4.5.3-1.4 构建初始化顺序

全局变量与全局常量之间可能存在相互依赖的情况，如下列代码所示：

```go
// 常量依赖其它常量
const a = b + 5
const b = 1

// 变量依赖其它常量、变量以及函数
const golang = "Golang"

var java = "Java"
var greeting = "Hello: " + lang(golang)

func lang(l string) string {
	if len(l) {
		return java
	}
	return l
}

// 循环依赖
const a = b
const b = c
const c = a + 1
```

为了简化初始化时的执行逻辑，我们需要确定变量的初始化顺序，即按照其依赖关系的拓扑顺序进行初始化，同时也需要检测是否有循环依赖。该逻辑定义在文件`$GCROOT/compile/internal/types2/initorder.go`中，入口方法是`check.initOrder()`。该方法的效果就是初始化 info.InitOrder 字段。

在讨论[Object对象](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.3-shu-ju-jie-gou-object-dui-xiang)时我们已经提到过，编译器特别定义了一个[dependency](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.3-shu-ju-jie-gou-object-dui-xiang#2-ju-ti-object-lei-xing)对象来标识那些可用于初始化表达式的对象，所有的 Const, Var 以及 Func 对象都属于 dependency 对象。

对象的依赖关系保存在[declInfo](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.5-lei-xing-jian-cha-qi#3-checker)的`deps`字段中，在对该对象进行类型检查时，检查器通过函数`func (d *declInfo) addDep(obj Object) {}`将当前对象所依赖的对象保存起来。

计算初始化顺序的算法思路如下：先构建对象依赖关系的有向图（Directed Graph），再以每个节点的依赖数目为权重构建最小堆（Min Heap）并以此堆作为最小优先级队列（Priority Queue），因此队列头部的对象总是依赖其它对象最少的，所以该队列的遍历顺序就是初始化的顺序。

info.InitOrder 内只包含全局变量的初始化逻辑，常量的初始化在类型检查时已经完成，这里处理常量的依赖图只是为了检测循环依赖。


# 4.5.3-1.5 总结

至此，整个类型检查的核心流程就梳理完毕了，类型检查器也已经将检查结果全部写到了 info 中。我们在这里只是尝试着搭起整个流程的骨架，各个步骤处理的细节无法一一涵盖，但只要把握了总体思路与代码框架，相信任何细节都可以顺藤摸瓜得找到了。


# 4.5.3-2 特定问题分析

在完成了类型检查的流程分析之后，本节我们将针对性地对一些问题进行分析，其中包括：

1. 对象循环依赖检查
2. 方法与属性的查找逻辑
3. Underlying Type

通过将这些问题与之前的主体逻辑相关联，能够让我们对整个类型检查的理解更加立体。


# 4.5.3-2a 对象循环依赖检查

在前文中我们分别讨论了[类型循环依赖](/golang-bian-yi-qi-lei-xing-jian-cha/4.5.31.3e-chu-li-delayed-dui-lie#xun-huan-yi-lai-jian-cha), 以及[构建初始化顺序](/golang-bian-yi-qi-lei-xing-jian-cha/4.5.31.4-gou-jian-chu-shi-hua-shun-xu)时初始化表达式的循环依赖问题，还有一个循环依赖问题需要考虑：Object 对象的循环依赖问题。

给定一个 Object 对象，类型检查的入口是函数`check.objDecl()`, 该函数的主要职责有两个：&#x20;

1. 对 Object 对象进行类型检查&#x20;
2. 检测被检查对象是否有循环依赖

在上文中我们已经详细讨论了第一个方面的内容，这里我们分析一下第二点。

类型检查器通过记录两个信息来做对象的循环依赖检测：&#x20;

1. 给对象标色[Object 接口](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.3-shu-ju-jie-gou-object-dui-xiang#1-jie-kou-ji-gong-yong-jie-gou)给对象的颜色定义了 getter 与 setter 方法（color 与 setColor），每个对象可以有三种不同的颜色： \
   a. 白色（white）：该对象还没有进行类型检查，这也是所有对象的初始颜色 \
   b. 灰色（grey）：该对象正在类型检查中 \
   c. 黑色（black）：该对象已经完成类型检查&#x20;
2. 记录对象检查的路径 对象依赖形成的结构是一个有向图（Directed Graph），类型检查会顺着依赖路径遍历所有的节点。[Checker](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.5-lei-xing-jian-cha-qi#3-checker)中定义了一个`objPath`属性，该属性用来记录当前类型检查所遍历过的所有对象，也就是当前的对象依赖路径。

结合这两点我们知道，在任意时刻，`checker.objPath`都保存着所有正在进行类型检查的对象，所以当`check.objDecl()`遇到一个灰色对象时，那么可以肯定我们就遇到了一个循环依赖，并且该对象已经在 objPath 上了。但并不是所有循环依赖都是错误的，例如：

```go
type A struct {
	parent *A
}
```

从 Object 层面来说，该结构体循环依赖于自己，但这种通过指针进行依赖是合法的，所以每次 objDecl 方法检测到循环依赖都会对其合法性进行校验，该方法由方法`check.cycle()`完成。我们知道：类型的合法性与变量的初始化顺序有专门的地方进行校验，所以该方法认为这两种情况合法，除此之外都是非法的。


# 4.5.3-2b 方法与属性查找

在两种情况下我们需要对目标类型进行属性、方法查找：&#x20;

1. 当编译器遇到形如`X.sel`的表达式时，其中`sel`可能是属性名，也可能是方法名&#x20;
2. 判断目标类型是否实现了某接口时，需要判断目标类型是否包含对应接口的所有方法

整个查找逻辑定义在文件`$GCROOT/compile/internal/types2/lookup.go`文件中，入口方法是`lookupFieldOrMethod()` 。Go 不允许方法重载（Method Overloading），并且属性与方法也能重名，所以对属性、方法查找的基本思路很简单，就是按照名字匹配就可以了。虽然如此，属性查找的逻辑仍然有一些点值得讨论：

### 内嵌类型属性的二义性检测

如果在类型中有重复的命名，不用等到属性查找阶段，编译器在对类型申明进行类型检查时就会发现错误，例如：

```go
  type A struct {
      name string
      name string // Compile error: name redeclared [DuplicateDecl]
  }

  func (this *A) name() {} // Compile error: Field and method with the same name name [DuplicateFieldAndMethod]
```

但我们知道对于内嵌类型，Go 会提升（Promote）内嵌类型的属性，例如：

```go
  type A struct {
      Name string
  }
  type B struct {
      A
      Addr string
  }

  func main() {
      var b B
      name := b.Name // 直接访问内嵌类型的字段
      addr := b.Addr
  }
```

但上述代码中 Go 并不会真的将 A 的属性 Name 抽取出来放入类型 B 中，这种“提升”其实只是一种便捷的访问方式，在对属性的查找过程中实现：如果当前类型的属性没有匹配项，则对下一级的内嵌类型进行查找，并依此逻辑递归直到查找完所有内嵌类型。这时便会面临一个问题：如果同一层级的内嵌类型有相同属性，那么编译器就不知道到底应该使用哪一个。例如代码：

```go
  type A struct {
      Name string
  }
  type B struct {
      Name string
  }
  type C struct {
      A
      B
  }

  func main() {
      var b B
      name := b.Name // Compile error: ambigous selector b.Name [AmbigousSelector]
  }
```

此时编译器会提示错误，从而引导用户提供更明确的选择，例如：`b.A.Name`

### 方法的等价性校验

方法`missingMethod(V Type, T Interface, static bool) (method, wrongType Func)`用来判断类型V是否实现了接口T，如果返回值为 nil, 则 V 实现了接口 T; 否则返回值表示查找到的第一个 V 没有实现 T 的方法。

这里涉及到的逻辑分为两个部分：一是对于 T 中的每个方法，按照方法名在 V 中进行查找；二是如果在 V 中找到了同名方法的话，判断两个方法是否是同一个类型，即判断是否拥有一致的参数列表与返回值列表。第一部分的思路与属性的查找一致；第二部分为了支持泛型，在此处并没有使用[类型的等价规则](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.412-lei-xing-de-deng-jia-gui-ze-fl)中的等价比较算法，这里叫着类型统一（[Type unification](https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#type-unification)），当前的实现逻辑在文件`$GCROOT/compile/internal/types2/unify.go`中，入口方法是`func (u *unifier) unify(x, y Type) bool {}`为什么叫类型统一呢？如果将类型想象成一个树形结构（或者图）的话，那么类型的等价规则的核心思想就是比较两个类型的结构是否一致，并且各对应结点的类型是否等价。这在没有类型参数的情况下是合理的，但是类型参数的引入使得该逻辑变得不够完善。在对[泛型数据结构](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.411-fan-xing-lei-xing-fl)的讨论中我们提到过类型模版的概念，意在说明包含泛型申明的类型可以表示不同的实际类型，例如，假如 T1 与 T2 都是类型参数，那么一个实际类型`map[int]bool`可以用如下类型来表示：

* T1 (T1 匹配 map\[int]bool, 或者直接用 T2 也可以)
* map\[T1]bool (T1 匹配 int)
* map\[int]T2 （T2 匹配 bool）
* map\[T1]T2 (T1 匹配 int, T2 匹配 bool)

  反过来，其无法被下述类型表示：
* \[]T1
* struct{}
* map\[T1]string

可见当引入类型参数后，在结构上不同的两个类型也可能是一个类型，所以此时的处理思路不再是一一对比，而是尝试着找到某种方法来将两个类型统一成同一个类型，从代数上理解的话，有点像寻找两个类型的公倍数。所以从实现上来说，该部分的逻辑如下：撇开类型参数，两个类型结构上必须一致，并且类型必须等价；如果有类型参数的话，那么一个类型的类型参数能够匹配另一个类型的类型参数的所有子类型。


# 4.5.3-2c Underlying Type

[接口](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.42-lei-xing-shu-ju-jie-gou-jie-kou)的申明中包含`Underlying() Type`这个方法，而所有的类型中只有[Named 类型](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.47-named-lei-xing-fl)的 Underlying Type 与自身不一样，其余所有类型的该方法都是返回自身。

在[类型表达式的类型检查](/golang-bian-yi-qi-lei-xing-jian-cha/4.5.31.3b-lei-xing-biao-da-shi-de-lei-xing-jian-cha)中我们知道编译器在做类型检查时，所有的类型信息都是通过函数`check.typ()`创建的，那么程序中的哪些表达式会导致调用该函数呢？答案是真正声明了一个类型的表达式，下列例子都会申明一个类型：

```go
int // 申明一个 int 类型
map[string]bool // 申明一个 map 类型
<- chan int // 申明一个 channel 类型
struct {/* 忽略 Fields */} // 申明一个 struct 类型
interface { /* 忽略方法申明 */ } // 申明一个 interface 类型
```

上面的每一行都会促使编译器创建一个类型，这种表达式叫着类型字面量（Type Literal），在[类型的等价规则](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.412-lei-xing-de-deng-jia-gui-ze-fl)中我们提到 Go 采用的是结构化类型系统，只要类型的结构一致，那么就是等价的，所以下列代码是合法的：

```go
func f(s struct {
	name string
	age  int
})

func main() {
	f(struct {
		name string
		age  int
	}{"golang", 12})
}
```

如果只能够通过字面量的方式创建类型，那就太繁琐了：在每个需要类型的地方都必须将类型的结构重新申明一遍。解决该问题的办法是为字面量类型取一个名字，该任务由`type`关键字完成。因此我们可以将所有`type`的申明分为三个部分： `type  <name> <type literal>`

* type: 关键字，提醒编译器接下来进入类型申明的代码块
* \<name>: 类型名字
* \<type literal>: 实际的类型值，该值将与 \<name> 绑定

这也是为何通过`type`关键字申明的类型叫`Named`的原因。而 \<type literal> 所代表的实际类型，就是`Named`的 Underlying Type.

有了对 Named 类型与 Underlying Type 的了解，我们就可以对与类型相关的很多行为有更深入的理解了。[Go Spec](https://golang.org/ref/spec#Types)中将类型概述为 "A type determines a set of values together with operations and methods specific to those values", 简单来说就是值与方法的集合。对于 Named 类型而言，通过类型结构可以发现：值保存在 Underly Type 中，而方法保存在Named 类型之中。所以在形如`X.sel`的表达式，在不考虑内嵌属性的情况下，如果 sel 是属性，则会从 Underlying Type 中查找，而如果 sel 是方法，则与 Underlying Type 无关。参见如下代码：

```go
type A struct {
	name string
}

func (this *A) getName() string {
	return "A: " + this.name
}

type B A

// 可以为类型 B 定义与类型 A 一样的方法
func (this *B) getName() string {
	return "B: " + this.name
}

type C B

func TestABC(t *testing.T) {
	var c C = C{name: "golang"} // OK, C 的 underlying type 是 A, 也有 name 属性

	_ = c.name      // 可以引用 name 属性
	_ = c.getName() // Compile error: c.getName undefined (type C has no field or method getName) [MissingFieldOrMethod]
}
```

上述代码中，类型 A, B, C 的 Underlying Type 都是`struct { name string }`, 对应[Struct 类型](/golang-bian-yi-qi-lei-xing-jian-cha/4.4.45-struct-lei-xing), 所以类型 C 引用属性 name 没有问题。而方法`getName()`定义在类型 A 上，所以通过类型 C 无法引用。对于申明`type name decl`, name 的 Underlying Type 确定逻辑如下：

```go
func under(t Type) Type {
	if n := asNamed(t); n != nil {
		return n.under()
	}
	return t
}
```

其中参数 t 是 decl 的类型，即如果 decl 是 Named 类型，则递归查找其 Underlying Type, 直到遇到非 Named 类型为止，则该类型就是整个链上所有 Named 类型的 Underlying Type.


# 4.6 如何测试

类型检查的测试比语法分析稍稍复杂一些，因为首先类型检查要依赖语法分析的输出，其次需要对类型检查器及整个编译环境进行初始化。

可以从`check_test.go`文件切入学习现有的测试代码， `examples`, `fixedbugs`以及`testdata`三个目录下包含大量的测试用例，对应的测试入口函数如下：

```go
func TestTestdata(t *testing.T)  { DefPredeclaredTestFuncs(); testDir(t, 75, "testdata") }
func TestExamples(t *testing.T)  { testDir(t, 0, "examples") }
func TestFixedbugs(t *testing.T) { testDir(t, 0, "fixedbugs") }
```

{% hint style="info" %}
Go 支持两种测试策略：黑盒测试与白盒测试。黑盒测试关注点在功能层面，测试代码仅允许访问被测试包的 Exported API ；而白盒测试关注点在实现层面，测试代码需要访问被测试包的所有内容。

Go 规范中约定测试文件与被测试文件在同一目录下，那么如何限制测试代码的访问权限呢？答案是将测试文件申明为不同的包。我们知道 Go 要求同一个目录下所有文件只能隶属于同一个包，但测试文件例外，测试文件的包名可以申明为`package xxx_test`, 其中 xxx 是被测试文件的包名，这样测试文件就处在单独的包中，以此达到黑盒测试的目的。
{% endhint %}

类型检查器采用的测试策略是黑盒测试，为了能够完成初始化，测试代码中已经实现了很多必要的辅助模块，例如`defaultImporter`, 同时语法解析也单独进行了封装。我们可以在`check_test.go`自定义如下方法来做测试：

```go
func TestTypecheck(t *testing.T) {
	code := `
package p

type A struct {}

func (this *A) name() {}

type B *A

func main() {
   var b B

  b.name()
}
`
	f, err := parseSrc("testTypecheckConst", code)

	if err != nil {
		panic(err)
	}

	// Dump 出语法树
	syntax.Fdump(os.Stdout, f)

	var conf Config
	conf.Trace = true
	conf.Importer = defaultImporter()
	conf.Error = func(err error) {
		fmt.Printf("Typecheck Error: %v\n", err)
	}

	info := Info{
		Types:      make(map[syntax.Expr]TypeAndValue),
		Defs:       make(map[*syntax.Name]Object),
		Uses:       make(map[*syntax.Name]Object),
		Selections: make(map[*syntax.SelectorExpr]*Selection),
		Implicits:  make(map[syntax.Node]Object),
		Scopes:     make(map[syntax.Node]*Scope),
		Inferred:   make(map[syntax.Expr]Inferred),
	}

	conf.Check("<no package>", []*syntax.File{f}, &info)
}
```

因为设置了`conf.Trace = true`, 我们将看到非常详细的检查结果，这里仅仅截取 main 函数的函数体的检查结果作为演示：

> ```
> == processDelayed ==
> testTypecheckConst:7:23:	--- name: func()
> testTypecheckConst:8:1:	--- <end>
> testTypecheckConst:12:13:	--- main: func()
> testTypecheckConst:13:10:	type B
> testTypecheckConst:13:10:	=> B (under = *A) // *types2.Named
> testTypecheckConst:15:7:	expr b.name()
> testTypecheckConst:15:2:	.  expr b.name
> testTypecheckConst:15:1:	.  .  expr b
> testTypecheckConst:15:1:	.  .  => b (variable of type B)
> testTypecheckConst:15:3:	.  .  ERROR: b.name undefined (type B has no field or method name)
> Typecheck Error: testTypecheckConst:15:3: b.name undefined (type B has no field or method name)
> testTypecheckConst:15:2:	.  => b.name (invalid operand)
> testTypecheckConst:15:7:	=> b.name() (invalid operand)
> testTypecheckConst:16:1:	--- <end>
> ```

对于方法调用`b.name()`, 类型检查器合理地报告了错误。

这样我们就可以任意修改源代码，并通过 UT 的方式查看效果，或者通过调试器查看每个细节是如何工作的了。


# 4.7 总结

至此，我们就完成了对 Go 类型检查器的总体逻辑分析。但类型检查的实现非常复杂，开发工作也一直在活跃进行，本文也只是努力尝试着描绘一个梗概，想要了解每个细节，还需要深入源码仔细学习。对于想要进一步学习的同学，作者鼓励通过如下方式进行：

* 把重放轻，熟悉数据结构及总体逻辑框架，不要纠缠于实现细节
* 有的放矢，如果有特别想要学习的部分，则找到想要学习的那部分代码细看。例如 Go 如何判断一个类型是否实现了某个接口。
* 反向推敲，多通过 UT 执行代码，多修改源码查看效果

对任何语言而言，类型系统都是很重要也很复杂的一个模块，本章中涉及到的知识虽然不会在日常的应用开发中直接用到，但对类型系统的深入理解对我们掌握 Go 语言大有裨益，甚至是精通 Go 语言的必要前提。掌握了编译器的内部实现之后，我们看待语言和理解程序的视角会发生根本性的改变。


# 5.1 简介

编译器的输入是源代码，输出是目标机器代码。编译器前端负责处理与源代码相关的逻辑，例如前面章节所讨论的词法分析、语法分析与类型检查；而编译器后端负责处理与目标代码相关的逻辑，例如目标代码生成、目标代码优化等。而中间代码（IR, Intermediate Representation）便是衔接前、后端的数据结构，因此可以将 IR 理解为编译器前端的输出以及编译器后端的输入。在一个模块化的编译器中，通常情况下 IR 即不会包含与任何源代码相关的信息，也不会包含与特定目标机器相关的信息，因此如果我们有 n 个语言的编译器前端与 m 个目标机器的编译器后端的话，便可以组合得到 n \* m 种编译程序，极大地减少了构建编译器的工作量。

可用于 IR(Intermediate Representation) 的数据结构多种多样，例如可以是与编程语言自然结构相似的树状结构，也可以是与目标指令结构相似的[三地址代码](https://en.wikipedia.org/wiki/Three-address_code)。事实上，在将源代码翻译成目标机器代码的过程中，编译器可能构造出一系列中间表示，例如编译器可能会先构造出树状结构的 IR, 因为该结构更接近源语言的层次结构，便于进行静态类型检查之类的处理；随后再构造出三地址代码的 IR, 这种结构与机器指令更加相似，适用于机器相关的任务处理，例如寄存器分配、指令选择等。

&#x20;Go 编译器在完成类型检查之后，首先会生成一颗基于 AST 的 IR Tree 进行函数内联、逃逸分析等操作，然后再生成基于 [SSA](https://en.wikipedia.org/wiki/Static_single_assignment_form) 的 IR 进行编译。本章介绍生成 IR Tree 的生成逻辑。


# 5.2 代码结构

> &#x20;兼容性说明：
>
> &#x20;在前言中提到 Go 的编译器团队当前正在重写类型检查系统，ir 包便是该过程中剥离出来的一部分，与 IR Tree 相关的代码后期应该都会移动到该包内。在老版本中，IR Tree 的生成与类型检查是同时完成的，新版本的实现做了隔离，在代码结构上更加清晰。
>
> &#x20;由于类型检查器的重写还没有完成，而 IR Tree 是很多后续处理的基础，因此现在 IR Tree 中依然关联了很多旧版本类型检查器的代码，主要包括目录 `cmd/compile/internal/types` 与 `cmd/compile/internal/typecheck` 中的代码。但我们并不需要将这些代码全部弄懂，只需要了解涉及到的一些基本的思路就可以继续学习下去，也许等到重写工作全部完成之后代码逻辑会更加清晰。

生成 IR Tree 的代码主要位于包 ir 中，在目录 `cmd/compile/internal/ir/` 下；还有部分在 `cmd/compile/internal/noder` 中。其中入口函数是 `cmd/compile/internal/noder/irgen.go` 中的 `irgen.generate()` ，ir 包是重写类型检查器时单独剥离出来的逻辑，因此该函数在新的类型检查函数 `check2()` 的最后阶段被调用。

所生成的 IR Tree 保存在全局变量 `typecheck.Target` 内，该变量定义在文件 `cmd/compile/internal/typecheck.target.go` 中，包含着当前被编译的包的所有信息，是所有后续操作的基础。


# 5.3 数据结构

## 5.3.1 IR Tree 简介 <a href="#org02e11eb" id="org02e11eb"></a>

既然在语法分析阶段已经生成了 AST, 为什么这里还需要另外构造一颗 IR Tree 呢？这是一个值得思考的问题！

语法分析阶段生成的 AST 在结构上与语言的文法相对应，其存在的主要价值是用来让编译器检查源程序是否符合文法结构，并做一定的语义分析，例如类型检查。但对于编译器的一些后续操作，例如代码优化、翻译时，这种结构便不是特别友好，例如对于代码 `make(chan string)` 与 `make(map[string]string)` 而言，在 AST 中都对应一个 `syntax.CallExpr` 节点，然而程序在运行时需要调用不同的内置函数来完成对应的操作（分别是 `makechan()` 与 `makemap()` ），因此编译器在翻译代码时最好能够直接知道这些信息，但这通过当前 AST 是做不到的（只能继续分析 syntax.CallExpr 的子节点来提取该信息）。我们可以这样认为：AST 中每个节点只反映了该程序片段的性质（上例中两个节点的性质都是方法调用），而编译器的很多后续操作需要知道更细节的信息，例如当前节点涉及到的具体操作（前者是 `makechan`, 而后者是 `makemap` ）。因此构建一种信息更加丰富的数据结构是有必要的，IR Tree 便是这样的一个数据结构。

## 5.3.2 IR Tree Node <a href="#orge616fec" id="orge616fec"></a>

&#x20;IR Tree 由 AST 转换而来，对于每一类 AST 节点，几乎都有一个 IR Tree 的节点类型与之对应。例如上文提到的 `syntax.CallExpr`, 其在语法分析阶段定义如下：

```go
// file: cmd/compile/internal/syntax/nodes.go
type CallExpr struct {
    Fun     Expr
    ArgList []Expr // nil means no arguments
    HasDots bool   // last argument is followed by ...
    expr
}
```

而在 IR Tree 中与之对应的结构体如下：

```go
// file: cmd/compile/internal/ir/expr.go
type CallExpr struct {
    miniExpr
    origNode
    X               Node
    Args            Nodes
    KeepAlive       []*Name // vars to be kept alive until call returns
    IsDDD           bool
    Use             CallUse
    NoInline        bool
    PreserveClosure bool // disable directClosureCall for this call
}
```

所有IR Tree 节点都实现了 `Node` 接口：

```go
// file: cmd/compile/internal/ir/node.go
type Node interface {
    // 格式化
    Format(s fmt.State, verb rune)

    // 源码位置信息
    Pos() src.XPos
    SetPos(x src.XPos)

    // 拷贝当前节点，在函数内联处理时会用到
    copy() Node

    // 对当前节点的子节点进行遍历并处理每个子节点
    doChildren(func(Node) bool) bool
    editChildren(func(Node) Node)

    // 节点的操作类型，后续有介绍
    Op() Op
    // 用来以一定顺序遍历本节点的子节点
    Init() Nodes

    // 类型信息，types.Type 是旧版本类型检查器所定义的类型，在生成 IR Tree 时会将 types2.Type 转换为该类型
    Type() *types.Type
    SetType(t *types.Type)
    // 用来表示所有命名对象的结构，后续会有介绍
    Name() *Name
    // 符号对象
    Sym() *types.Sym
    // 如果当前 Node 是常量，用来保存常量值
    Val() constant.Value
    SetVal(v constant.Value)

    // 用来逃逸分析
    Esc() uint16
    SetEsc(x uint16)
    Diag() bool
    SetDiag(x bool)

    // Typecheck values:
    //  0 means the node is not typechecked
    //  1 means the node is completely typechecked
    //  2 means typechecking of the node is in progress
    //  3 means the node has its type from types2, but may need transformation
    Typecheck() uint8
    SetTypecheck(x uint8)
    NonNil() bool
    MarkNonNil()
}
```

`Node` 是个大杂烩接口，各类节点按需实现各方法，并不是所有的节点都需要用到所有的方法。各种类型的节点主要定义在下列文件中：

* cmd/compile/internal/ir/type.go
* cmd/compile/internal/ir/expr.go
* cmd/compile/internal/ir/stmt.go
* cmd/compile/internal/ir/func.go
* cmd/compile/internal/ir/name.go

由于 IR Tree 是 AST 的一种变体，二者刻画了同一个程序结构，所以这里就不再一一介绍 IR Tree 节点了。但由于 IR Tree 中的节点更加具体，为了支持编译器后续各个阶段的处理，某些节点内包含的信息更加丰富全面，我们重点介绍一下 `Name` 与 `Func` 这两个节点。

## 5.3.3 Name <a href="#org40f6b70" id="org40f6b70"></a>

节点类型 `Name` 用来在 IR Tree 中表示程序中的命名对象，源代码中所有有名字的对象都会对应一个 `Name` 节点，命名对象包括全局函数申明、全局类型申明、全局变量、局部变量、函数参数、甚至函数返回值等。

程序运行时使用的内存被划分为两块：栈（Stack）与堆（Heap）。栈在函数调用时自动分配，在函数执行结束时自动释放，其生命周期与函数调用一致，栈内的数据仅对当前函数可见；而堆是共享的内存块，堆的分配与释放需要单独进行管理，负责回收堆内存的程序模块叫着GC(Garbage Collector)。所以对于函数的局部变量，如果函数执行完成后其再也不会被访问，则为其分配栈上的内存更加简单，可以减轻 GC 的压力；而如果其生命周期长于函数的生命周期，则必须将其分配到堆上，由 GC 决定何时回收。

Go 提供了自动管理内存的功能，对于每个局部变量，编译器都会对其进行分析，然后决定为其分配栈上的内存还是堆上的内存。这个过程叫着逃逸分析（Escape Analysis），后续会有专门的章节进行介绍，而局部变量所对应的 `Name` 节点会记录逃逸分析的结果。

`Name` 节点是程序中非常重要的部分，符号解析、内存分配、函数调用等都与该节点相关，该结构定义在文件 `cmd/compile/internal/ir/name.go` 中，其包含的属性覆盖了编译器后续处理时需要的所有信息：

```go
type Name struct {
    miniExpr
    BuiltinOp Op         // 如果是内置函数，则对应内置函数的 Op 常量
    Class     Class      // uint8
    pragma    PragmaFlag // int16
    flags     bitset16
    sym       *types.Sym
    Func      *Func // TODO(austin): nil for I.M, eqFor, hashfor, and hashmem
    Offset_   int64
    val       constant.Value
    Opt       interface{} // 用于逃逸分析
    Embed     *[]Embed    // list of embedded files, for ONAME var

    PkgName *PkgName // real package for import . names
    // For a local variable (not param) or extern, the initializing assignment (OAS or OAS2).
    // For a closure var, the ONAME node of the outer captured variable
    Defn Node

    // The function, method, or closure in which local variable or param is declared.
    Curfn *Func

    Ntype    Ntype
    Heapaddr *Name // temp holding heap address of param

    Innermost *Name
    Outer     *Name
}
```

## 5.3.4 Func <a href="#orgd513060" id="orgd513060"></a>

函数是编译器处理的重点对象，说其是最重要的对象也不为过，像逃逸分析、闭包处理、函数内联、以及代码编译的处理对象都是函数。因此该结构体内是 IR Tree 中最复杂的，包含很多属性。代码中的函数、闭包、方法都使用 `Func` 来表示，其定义在 `cmd/compiler/internal/ir/func.go` 中：

```go
type Func struct {
    miniNode
    Body Nodes // 函数体
    Iota int64

    Nname    *Name        // 函数名对应的 Name 节点
    OClosure *ClosureExpr // OCLOSURE node

    Shortname *types.Sym

    // Extra entry code for the function. For example, allocate and initialize
    // memory for escaping parameters.
    Enter Nodes
    Exit  Nodes

    // 保存函数体内的绑定变量（Bound Variable），即在函数内声明的局部变量，包括参数及返回值，后面逃逸分析时会用到
    Dcl []*Name

    // 保存函数体内的自由变量（Free Variable），即该变量在当前函数体内使用，但在更外层的函数体内声明
    // 例如：
    // var base int
    // func makeAdder(i int)func(int)int  {
    //      return func  (j int) int {
    //                return i + j + base
    //            }
    // }
    // 对于作为返回值的函数而言，j 是 Bound Variable, i 是 Free Variable. 需要注意的是全局变量不属于 Free Variable, 即该例中的 base 不会保存在该属性中
    ClosureVars []*Name

    // 保存函数内的闭包，在编译阶段填充。见 walk.Walk() 方法
    Closures []*Func

    Parents []ScopeID

    // Marks records scope boundary changes.
    Marks []Mark

    FieldTrack map[*obj.LSym]struct{}
    DebugInfo  interface{}
    LSym       *obj.LSym // Linker object in this function's native ABI (Func.ABI)

    Inl *Inline // 保存能够内联的函数体，函数内联的章节有详细介绍

    Closgen int32

    Label int32 // largest auto-generated label in this function

    Endlineno src.XPos
    WBPos     src.XPos // position of first write barrier; see SetWBPos

    Pragma PragmaFlag // go:xxx function annotations

    flags bitset16

    // ABI 相关字段，ABI 章节有详细介绍
    ABI     obj.ABI
    ABIRefs obj.ABISet

    NumDefers  int32 // number of defer calls in the function
    NumReturns int32 // number of explicit returns in the function

    NWBRCalls *[]SymAndPos
}
```

函数节点是编译器后续处理的重要对象，该结构体内包含的属性也贯穿整个编译过程，后续章节都会围绕着该节点展开。

## 5.3.5 Node Op <a href="#org273d76f" id="org273d76f"></a>

前文中提到 IR Tree 的节点会体现具体操作， `Node` 接口的方法 `Op() Op` 反映了这一点。在文件 `cmd/compile/internal/ir/node.go` 中编译器定义了差不多 200 种具体操作，这里罗列其中一部分：

```go
type Op uint8

const (
    OXXX Op = iota

    // names
    ONAME // var or func name
    // Unnamed arg or return value: f(int, string) (int, error) { etc }
    // Also used for a qualified package identifier that hasn't been resolved yet.
    ONONAME
    OTYPE    // type name
    OPACK    // import
    OLITERAL // literal
    ONIL     // nil

    // expressions
    OADD       // Left + Right
    OSUB       // Left - Right
    OADDSTR    // +{List} (string addition, list elements are strings)
    OADDR      // &Left
    OANDAND    // Left && Right
    OAPPEND    // append(List); after walk, Left may contain elem type descriptor
    OBYTES2STR // Type(Left) (Type is string, Left is a []byte)

    OOROR  // Left || Right
    OPANIC // panic(Left)
    OPRINT // print(List)
    OPAREN // (Left)
    OSEND  // Left <- Right
    OSLICE // Left[List[0] : List[1]] (Left is untypechecked or slice)
)   
```

Op 所定义的操作非常详细，各个操作符、内置函数、关键字都有对应的操作，每个 IR Tree 的节点都包含该字段，该字段也是编译器很多后续处理的基础。

## 5.3.6 types.Sym <a href="#org031bb5d" id="org031bb5d"></a>

`types.Sym` 用来表示包中的一个符号对象，其定义在 `cmd/compile/internal/types/sym.go` 中，内容如下：

```go
type Sym struct {
    Linkname string // link name

    Pkg  *Pkg
    Name string // object name

    // saved and restored by Pushdcl/Popdcl
    Def        Object   // definition: ONAME OTYPE OPACK or OLITERAL
    Block      int32    // blocknumber to catch redeclaration
    Lastlineno src.XPos // last declaration for diagnostic

    flags bitset8
}
```

编译器做符号解析时，得到的结果就是该类型的实例，属性 (Pkg, Name) 的组合唯一确定一个符号对象。

## 5.3.7 types.Pkg <a href="#org2f8fd01" id="org2f8fd01"></a>

`types.Pkg` 用来封装通过 import 语句引入的 package 的内容，其内容如下：

```go
// file: cmd/compiler/internal/types/pkg.go
type Pkg struct {
    Path    string // string literal used in import statement, e.g. "runtime/internal/sys"
    Name    string // package name, e.g. "sys"
    Prefix  string // escaped path for use in symbol table
    Syms    map[string]*Sym
    Pathsym *obj.LSym

    Height int
    Direct bool // imported directly
}
```

属性 `Syms` 存放着该包的所有 Export 的符号对象。

## 5.3.8 types.Type <a href="#org9598c27" id="org9598c27"></a>

`types.Type` 是老版本类型检查器中用来表示类型的数据结构，当前编译器的所有后续操作依然是基于该类型进行的，所以在 IR Tree 的构建过程中，需要将 types2 中定义的类型转换成对应的 types.Type, 该结构体定义在文件 `cmd/compile/internal/noder/types.go` 中，其中有一些属性我们特别提出来认识一下：

```go
type Type struct {
    // 省略掉其他所有字段
    Width int64 // 该类型所占用的字节大小
    Align uint8 // 该类型的地址对齐要求
}
```

在编译阶段，编译器需要计算出每种类型所占作用的内存大小，以及地址对齐的要求。关于 Go 类型大小及地址对齐的计算方式请参见 [ABI Specification](https://go.googlesource.com/go/+/refs/heads/dev.regabi/src/cmd/compile/internal-abi.md#memory-layout)


# 5.4 处理逻辑

## 5.4.1 相关代码 <a href="#org45ad279" id="org45ad279"></a>

与构建 IR Tree 密切相关的代码存在于两个地方，一个在文件 `cmd/compiler/internal/noder/irgen.go` 中，是构建逻辑的入口；另一个在 `cmd/compiler/internal/typecheck/target.go` 中，该文件只定义了一个全局变量 `Target`, 而该全局变量保存着 IR Tree 构建的全部结果。

全局变量 `Target` 的类型定义在 `cmd/compiler/internal/ir/package.go` 中，内容如下：

```go
type Package struct {
    Imports []*types.Pkg // <<Imports>>

    // Init functions, listed in source order.
    Inits []*Func

    // Top-level declarations.
    Decls []Node

    // Extern (package global) declarations.
    Externs []Node

    // Assembly function declarations.
    Asms []*Name

    // Cgo directives.
    CgoPragmas [][]string

    // Variables with //go:embed lines.
    Embeds []*Name

    // Exported (or re-exported) symbols.
    Exports []*Name

    // Map from function names of stencils to already-created stencils.
    Stencils map[*types.Sym]*Func
}
```

回忆一下：package 是 Go 编译器一次编译的最大单元！该结构用来封装被编译的包的所有信息。构建 IR Tree 的过程便是填充其中内容的过程，自此往后，编译器的操作都是基于这里面的内容来完成了。

IR Tree 构建逻辑由 `irgen` 驱动，其定义如下：

```go
// file: cmd/compiler/internal/noder/irgen.go
type irgen struct {
    target *ir.Package // 当前对应全局变量 typecheck.Target
    self   *types2.Package
    info   *types2.Info // types2 类型检查的结果

    posMap
    objs   map[types2.Object]*ir.Name
    typs   map[types2.Type]*types.Type
    marker dwarfgen.ScopeMarker

    // Fully-instantiated generic types whose methods should be instantiated
    instTypeList []*types.Type
}
```

构建入口及绝大多数逻辑都由 irgen上的方法来完成。

## 5.4.2 构建入口及步骤 <a href="#orgf0ac657" id="orgf0ac657"></a>

构建逻辑的入口方法是 `irgen.generate()` ，其中包含着总体的处理步骤，我们在此处仅保留处理框架一窥大概：

```go
func (g *irgen) generate(noders []*noder) {
    declLists := make([][]syntax.Decl, len(noders))
Outer:
    // Step 1: 处理 import 语句，将对应的包加载并解析成 types.Pkg 对象，本质上是根据 import 语句实例化 typecheck.Target.Imports 字段
    for i, p := range noders {
        g.pragmaFlags(p.file.Pragma, ir.GoBuildPragma)
        for j, decl := range p.file.DeclList {
            switch decl := decl.(type) {
            case *syntax.ImportDecl:
                // 处理 Import 语句
                g.importDecl(p, decl)
            default:
                // 非 Import 语句保存起来留着后续步骤处理
                declLists[i] = p.file.DeclList[j:]
                continue Outer // no more ImportDecls
            }
        }
    }

    // Step 2: 处理全局的类型申明，对于每个全局类型申明，创建一个对等的 ir.Decl 对象并将其存入 typecheck.Target.Decls 中
    // 1. 创建 Name 对象
    // 2. 将 types2.Type 转换为对应的 types.Type
    // 3. 处理该类型的方法，创建对应的 Field 对象
    for _, declList := range declLists {
        for _, decl := range declList {
            switch decl := decl.(type) {
            case *syntax.TypeDecl:
                g.typeDecl((*ir.Nodes)(&g.target.Decls), decl)
            }
        }
    }

    // Step 3: 处理其他类型的申明，即全局常量申明、全局变量申明、函数申明。为每个申明创建出对应的 ir.Node 并保存在 typecheck.Target.Decls 中
    for _, declList := range declLists {
        // shizhz -
        g.target.Decls = append(g.target.Decls, g.decls(declList)...)
    }

    // Step 4: 注册所有的 builtin 函数到 types.LocalPkg 中
    typecheck.DeclareUniverse()

    // Step 5: 根据泛型函数使用情况创建函数，该过程叫着泛型函数的实例化，后续会专门介绍
    g.stencil()

    // Step 6: 去掉所有的泛型函数申明，详见后续关于第5步的介绍
    j := 0
    for i, decl := range g.target.Decls {
        if decl.Op() != ir.ODCLFUNC || !decl.Type().HasTParam() {
            g.target.Decls[j] = g.target.Decls[i]
            j++
        }
    }
    g.target.Decls = g.target.Decls[:j]
}
```

注意，方法 `irgen.generate()` 的参数 `[]*noder` 便是语法分析的结果：AST. 通过上述代码我们基本可以总结出 IR Tree 构建的总体逻辑：遍历 AST 并构建出 IR Tree, 并将所有的处理结果保存在全局变量 `typecheck.Target` 中。比较特别的是 Step 5 与 Step 6, 这两步将泛型函数的调用完全转换成了普通函数的调用，之后所有源代码中关于泛型的语义就消失了。

下面我们详细探索一下上述各步骤的处理逻辑。

## 5.4.3 Import 语句 <a href="#org356f657" id="org356f657"></a>

在类型检查章节，我们已经介绍过新类型检查器中有关包（package）的数据结构以及包加载器（Package Loader）的工作原理，但这部分内容当前还没有应用到编译器后续步骤中来，所以在 IR Tree 的构建阶段依然采用的是之前的加载逻辑，其总体逻辑是将一个 import 语句解析成一个 `types.Pkg` 对象并保存到 `typecheck.Target` 的 [Imports](/golang-bian-yi-qi-ir-tree/untitled-2#org45ad279) 属性中。

处理 import 语句对应前面的 Step 1, 代码细节这里不再详细展开。

## 5.4.4 翻译 AST <a href="#org44645ef" id="org44645ef"></a>

将 AST 翻译成 IR Tree 的对应上述 Step 2 与 Step 3. 这里我们先看 Step 3 入口函数的代码：

```go
func (g *irgen) decls(decls []syntax.Decl) []ir.Node {
    var res ir.Nodes
    for _, decl := range decls {
        switch decl := decl.(type) {
        case *syntax.ConstDecl:
            g.constDecl(&res, decl)
        case *syntax.FuncDecl:
            g.funcDecl(&res, decl)
        case *syntax.TypeDecl:
            if ir.CurFunc == nil {
                continue // already handled in irgen.generate
            }
            g.typeDecl(&res, decl)
        case *syntax.VarDecl:
            g.varDecl(&res, decl)
        default:
            g.unhandled("declaration", decl)
        }
    }
    return res
}
```

函数通过 `switch...case...` 对 AST 进行深度优先遍历，并且在遍历过程中根据 AST 的节点构建出对应的 IR Tree 节点。当然 AST 的节点与 IR Tree 的节点并不是一一对应的，例如对于赋值语句 `names := make(chan string)`, AST 与 IR Tree 的结构如下：

![AST vs IR Tree](/files/-MamBT71CuF-Y77GV63X)

> &#x20;上图中椭圆形代表树的节点，线段代表属性名称，矩形代表属性值。图中只显示了重要的属性值。

可见 IR Tree 与 AST 刻画了相同的程序结构，但是 IR Tree 的节点信息更加具体，例如对于 `make(chan string)`, IR Tree 中对应节点的属性就比 AST 中的节点更有表现力。

构建 IR Tree 的逻辑要点可以归结如下：

1. 翻译节点，例如上图中将 AST 中的 `CallExpr` 节点翻译成更加具体的 `MakeExpr` 节点。核心代码如下：
   * `cmd/compile/internal/noder/stmt.go` 中的 `irgen.stmt()` 方法用来翻译语句（Statement）
   * `cmd/compile/internal/noder/expr.go` 中的 `irgen.expr()` 方法用来翻译表达式（Expression）
2. 翻译类型，将新类型检查器中所使用的 `types2` 类型翻译成 `types` 下对应的类型，例如上图中的橙色方框。核心代码如下：
   * `cmd/compile/internal/noder/types.go` 中的 `irgen.typ()` 方法用来将 `types2` 中的类型转换成 `types` 中的类型。

上述的 Step 2 与 Step 3 会构建出完整的 IR Tree, Step 2 先对全局的类型申明（即通过 `type` 关键字申明的类型）进行转换，Step 3 对剩下的全局申明进行转换，例如常量、变量、函数申明等。所有的结果都会保存到[typecheck.Target](/golang-bian-yi-qi-ir-tree/untitled-2#org45ad279)的 Decls 属性中，这两步完成之后，该属性保存着完整的 IR Tree.

## 5.4.5 告别泛型 <a href="#org3e1ddf1" id="org3e1ddf1"></a>

在类型检查章节中，我们讨论过泛型的实例化，泛型相当于定义了一个模版，而使用泛型的地方需要根据该模版生成实际对象。对于泛型函数而言，实例函数的生成发生在构建 IR Tree 的最后阶段。我们先了解一下泛型函数的实例化，对于代码：

```go
package main

import "fmt"

func genericFun[T any](i, j T)  {
    fmt.Printf("i: %v, j: %v\n",i, j)
}

func main() {
    genericFun(5,6)
    genericFun[float32](10.5, 11.8)
}
```

这里有两个地方使用到了泛型函数 `genericFun`, 如果不适用泛型的话，上述代码等价于：

```go
package main

import "fmt"

func genericFunInt(i, j int) {
    fmt.Printf("i: %v, j: %v\n", i, j)
}

func genericFunFloat32(i, j float32) {
    fmt.Printf("i: %v, j: %v\n", i, j)
}

func main() {
    genericFunInt(5, 6)
    genericFunFloat32(10.5, 11.8)
}
```

前者代码量更少，对编程人员友好，后者虽然代码量更大，但编译器处理起来更简单，实际上 Go 在不支持泛型时编译的就是这样的代码，而只要明确了使用泛型函数时的类型参数，我们就可以轻松地将前者转换为后者。根据类型参数创建普通函数，便是泛型函数的实例化。

我们知道当编译器完成类型检查后，所有函数调用的类型信息都已经明确了，对泛型的使用也是如此：要么代码中显示指定了类型参数，例如上例中的 `genericFun[float32](10.5, 11.8)` ；要么类型检查器通过类型推导得到类型信息，例如上例中的 `genericFun(5,6)`. 因此在编译阶段我们就可以完成泛型函数的实例化，事实上编译器实例化效果与上述示例代码完全一样，只有生成的函数名不同，对于上述代码，编译器使用的实例化函数名字分别是 `genericFun[int]` 与 `genericFun[float32]`, 这种函数名称在代码中是非法的，仅在编译器内部使用。

泛型函数的实例化会改变 IR Tree 的结构及内容，一是创建出了新的全局函数申明，需要将其添加到 IR Tree 中，二是函数调用的节点需要修改，三是再完成实例化后，泛型函数的申明就不再需要了，即我们可以从 IR Tree 中将其删除。前两点在 Step 5 中通过函数 `irgen.stencil()` 完成，所有的相关逻辑都定义在文件 `cmd/compile/internal/stencil.go` 中。而最后一步 Step 6 则删除了 IR Tree 中所有的泛型函数申明。

自此，我们的代码就告别泛型，又回归最传统的形式了。


# 5.5 编译日志

如果查看 `irgen.generate()` 的完整代码，可以发现如下代码片段：

```go
if base.Flag.W > 1 {
    for _, n := range g.target.Decls {
        s := fmt.Sprintf("\nafter noder2 %v", n)
        ir.Dump(s, n)
    }
}
```

可见 IR Tree 并没有完全隐藏在编译器内部，而是可以通过编译参数 `-W` 来查看，并且编译器不仅会 dump 出 IR Tree 的结构，还会 dump 出泛型函数实例化时所做的改变。我们通过一个例子来查看一下。

将下列代码保存为 main.go:

```go
package main

import "fmt"

func genericFun[T any](i, j T)  {
    fmt.Printf("i: %v, j: %v\n",i, j)
}

func main() {
    genericFun(5,6)
    genericFun[float32](10.5, 11.8)
}
```

通过命令 `go tool compile -G=2 -W=2 main.go` 进行编译即可看到如下结果：

> ```
> after noder2 genericFun [0xc0001642c0]
> .   DCLFUNC tc(1) Iota:-1 ABI:ABIInternal FUNC-func[T₁](T₁, T₁) # main.go:5
> .   DCLFUNC-Dcl
> .   .   NAME-main.i tc(1) Class:PPARAM Offset:0 OnStack main.T₁ # main.go:5
> .   .   NAME-main.j tc(1) Class:PPARAM Offset:0 OnStack main.T₁ # main.go:5
> .   DCLFUNC-body
> .   .   CALLFUNC tc(1) Use:3 STRUCT-(int, error) # main.go:6 STRUCT-(int, error)
> .   .   .   NAME-fmt.Printf tc(1) Class:PFUNC Offset:0 FUNC-func(string, …interface {}) (int, error) # print.go:212
> .   .   CALLFUNC-Args
> .   .   .   LITERAL-“i: %v, j: %v\n” tc(1) string # main.go:6
> .   .   .   CONVIFACE tc(1) Implicit INTER-interface {} # main.go:6 INTER-interface {}
> .   .   .   .   NAME-main.i tc(1) Class:PPARAM Offset:0 OnStack main.T₁ # main.go:5
> .   .   .   CONVIFACE tc(1) Implicit INTER-interface {} # main.go:6 INTER-interface {}
> .   .   .   .   NAME-main.j tc(1) Class:PPARAM Offset:0 OnStack main.T₁ # main.go:5
>
> after noder2 main [0xc000164580]
> .   DCLFUNC tc(1) Iota:-1 ABI:ABIInternal FUNC-func() # main.go:9
> .   DCLFUNC-body
> .   .   CALL tc(1) Use:3 # main.go:10
> .   .   .   FUNCINST tc(1) FUNC-func[T₁](T₁, T₁) # main.go:10 FUNC-func[T₁](T₁, T₁)
> .   .   .   .   NAME-main.genericFun tc(1) Class:PFUNC Offset:0 FUNC-func[T₁](T₁, T₁) # main.go:5
> .   .   .   FUNCINST-Targs
> .   .   .   .   TYPE .int Offset:0 type int
> .   .   CALL-Args
> .   .   .   LITERAL-5 tc(1) int # main.go:10
> .   .   .   LITERAL-6 tc(1) int # main.go:10
> .   .   CALL tc(1) Use:3 # main.go:11
> .   .   .   FUNCINST tc(1) FUNC-func[T₁](T₁, T₁) # main.go:11 FUNC-func[T₁](T₁, T₁)
> .   .   .   .   NAME-main.genericFun tc(1) Class:PFUNC Offset:0 FUNC-func[T₁](T₁, T₁) # main.go:5
> .   .   .   FUNCINST-Targs
> .   .   .   .   TYPE .float32 Offset:0 type float32
> .   .   CALL-Args
> .   .   .   LITERAL-10.5 tc(1) float32 # main.go:11
> .   .   .   LITERAL-11.8 tc(1) float32 # main.go:11
>
> stenciled genericFun[int] [0xc0001646e0]
> .   DCLFUNC tc(1) Iota:-1 ABI:ABIInternal FUNC-func(int, int) # main.go:5
> .   DCLFUNC-Dcl
> .   .   NAME-main.i tc(1) Class:PPARAM Offset:0 OnStack int # main.go:5
> .   .   NAME-main.j tc(1) Class:PPARAM Offset:0 OnStack int # main.go:5
> .   DCLFUNC-body
> .   .   CALLFUNC tc(1) Use:3 STRUCT-(int, error) # main.go:6 STRUCT-(int, error)
> .   .   .   NAME-fmt.Printf tc(1) Class:PFUNC Offset:0 FUNC-func(string, …interface {}) (int, error) # print.go:212
> .   .   CALLFUNC-Args
> .   .   .   LITERAL-“i: %v, j: %v\n” tc(1) string # main.go:6
> .   .   .   CONVIFACE tc(1) Implicit INTER-interface {} # main.go:6 INTER-interface {}
> .   .   .   .   NAME-main.i tc(1) Class:PPARAM Offset:0 OnStack int # main.go:5
> .   .   .   CONVIFACE tc(1) Implicit INTER-interface {} # main.go:6 INTER-interface {}
> .   .   .   .   NAME-main.j tc(1) Class:PPARAM Offset:0 OnStack int # main.go:5
>
> stenciled genericFun[float32] [0xc000164840]
> .   DCLFUNC tc(1) Iota:-1 ABI:ABIInternal FUNC-func(float32, float32) # main.go:5
> .   DCLFUNC-Dcl
> .   .   NAME-main.i tc(1) Class:PPARAM Offset:0 OnStack float32 # main.go:5
> .   .   NAME-main.j tc(1) Class:PPARAM Offset:0 OnStack float32 # main.go:5
> .   DCLFUNC-body
> .   .   CALLFUNC tc(1) Use:3 STRUCT-(int, error) # main.go:6 STRUCT-(int, error)
> .   .   .   NAME-fmt.Printf tc(1) Class:PFUNC Offset:0 FUNC-func(string, …interface {}) (int, error) # print.go:212
> .   .   CALLFUNC-Args
> .   .   .   LITERAL-“i: %v, j: %v\n” tc(1) string # main.go:6
> .   .   .   CONVIFACE tc(1) Implicit INTER-interface {} # main.go:6 INTER-interface {}
> .   .   .   .   NAME-main.i tc(1) Class:PPARAM Offset:0 OnStack float32 # main.go:5
> .   .   .   CONVIFACE tc(1) Implicit INTER-interface {} # main.go:6 INTER-interface {}
> .   .   .   .   NAME-main.j tc(1) Class:PPARAM Offset:0 OnStack float32 # main.go:5
>
> modified main [0xc000164580]
> .   DCLFUNC tc(1) Iota:-1 ABI:ABIInternal FUNC-func() # main.go:9
> .   DCLFUNC-body
> .   .   CALLFUNC tc(1) Use:3 # main.go:10
> .   .   .   NAME-main.genericFun[int] tc(1) Class:PFUNC Offset:0 FUNC-func(int, int) # main.go:5
> .   .   CALLFUNC-Args
> .   .   .   LITERAL-5 tc(1) int # main.go:10
> .   .   .   LITERAL-6 tc(1) int # main.go:10
> .   .   CALLFUNC tc(1) Use:3 # main.go:11
> .   .   .   NAME-main.genericFun[float32] tc(1) Class:PFUNC Offset:0 FUNC-func(float32, float32) # main.go:5
> .   .   CALLFUNC-Args
> .   .   .   LITERAL-10.5 tc(1) float32 # main.go:11
> .   .   .   LITERAL-11.8 tc(1) float32 # main.go:11
> ```

前两个以 `after noder2` 开头的结构分别对应两个函数申明，这是 Step 3 完成之后 IR Tree 的总体结构，可以看到其中还包括泛型信息；而随后的信息是泛型函数实例化的 dump 信息，以 `stenciled` 开头的两个函数 `genericFun[int]` 与 `genericFun[float32]` 便是生成的实例化函数，最后 `modified main` 是修改之后的 main 函数，可以发现此时 main 函数内部直接调用的是实例化函数，已经抹掉了有关泛型的所有信息。

编译参数加上 `-G=2` 是为了启用泛型支持以便调用新的类型检查逻辑，并且在创建 IR Tree 完成之后退出编译，否则会 dump 出非常多的信息。可以在 `irgen.go` 的函数 `check2()` 中查看该 flag 的使用方式。


# 5.6 Unit Test

IR Tree 构建逻辑的 UT 比前面章节的要麻烦一些，因为其不仅依赖前面的词法分析、语法分析、类型检查等所有步骤，还要依赖整个编译器的全局环境初始化。

编译器的初始化工作在文件 `cmd/compile/internal/gc/main.go` 中的 `Main` 函数中完成，词法分析之前的所有逻辑都是在做初始化的工作，分割点如下所示：

```go
func Main(archInit func(*ssagen.ArchInfo)) {
    // 初始化代码，此处省略

    // 词法分析、语法分析、类型检查
    noder.LoadPackage(flag.Args())

    // 编译阶段后续步骤，此处省略
}
```

编译器的初始化工作涉及很多方面，例如与架构体系相关的初始化（例如 amd64, x86 等）、内置函数及类型、默认配置等，但也有很多代码是搭建 UT 环境不需要的，例如解析编译参数、对编译过程进行 profile 等。为了搭建 IR Tree 的 UT 环境，我们需要做如下工作：

* 精简后的初始化代码
* 语法解析、类型检查等步骤
* Dump IR Tree 的逻辑

我们创建 UT 文件 `cmd/compile/internal/noder/irgen_test.go`, 其内容如下：

```go
// file: cmd/compile/internal/noder/irgen_test.go

package noder

import (
    "bufio"
    "cmd/compile/internal/amd64"
    "cmd/compile/internal/base"
    "cmd/compile/internal/escape"
    "cmd/compile/internal/inline"
    "cmd/compile/internal/ir"
    "cmd/compile/internal/reflectdata"
    "cmd/compile/internal/ssa"
    "cmd/compile/internal/ssagen"
    "cmd/compile/internal/syntax"
    "cmd/compile/internal/typecheck"
    "cmd/compile/internal/types"
    "cmd/compile/internal/types2"
    "cmd/internal/obj"
    "cmd/internal/objabi"
    "cmd/internal/src"
    "fmt"
    "os"
    "strings"
    "testing"
)

func parseSrc(path, src string) (*syntax.File, error) {
    var mode syntax.Mode
    mode = syntax.AllowGenerics | syntax.CheckBranches
    errh := func(error) {} // dummy error handler so that parsing continues in presence of errors
    return syntax.Parse(syntax.NewFileBase(path), strings.NewReader(src), errh, nil, mode)
}

func defaultImporter() *gcimports {
    return &gcimports{
        packages: map[string]*types2.Package{},
    }
}

func init() {
    amd64.Init(&ssagen.Arch)

    base.Ctxt = obj.Linknew(ssagen.Arch.LinkArch)
    base.Ctxt.DiagFunc = base.Errorf
    base.Ctxt.DiagFlush = base.FlushErrors
    base.Ctxt.Bso = bufio.NewWriter(os.Stdout)

    base.Ctxt.UseBASEntries = base.Ctxt.Headtype != objabi.Hdarwin

    types.LocalPkg = types.NewPkg("", "")
    types.LocalPkg.Prefix = "\"\""

    types.LocalPkg.Height = types.MaxPkgHeight

    types.BuiltinPkg = types.NewPkg("go.builtin", "") // TODO(gri) name this package go.builtin?
    types.BuiltinPkg.Prefix = "go.builtin"            // not go%2ebuiltin

    ir.Pkgs.Unsafe = types.NewPkg("unsafe", "unsafe")

    ir.Pkgs.Runtime = types.NewPkg("go.runtime", "runtime")
    ir.Pkgs.Runtime.Prefix = "runtime"

    ir.Pkgs.Itab = types.NewPkg("go.itab", "go.itab")
    ir.Pkgs.Itab.Prefix = "go.itab" // not go%2eitab

    ir.Pkgs.Go = types.NewPkg("go", "")

    base.DebugSSA = ssa.PhaseOption

    ssagen.Arch.LinkArch.Init(base.Ctxt)
    ir.EscFmt = escape.Fmt
    ir.IsIntrinsicCall = ssagen.IsIntrinsicCall
    inline.SSADumpInline = ssagen.DumpInline
    ssagen.InitEnv()
    ssagen.InitTables()

    types.PtrSize = ssagen.Arch.LinkArch.PtrSize
    types.RegSize = ssagen.Arch.LinkArch.RegSize
    types.MaxWidth = ssagen.Arch.MAXWIDTH

    typecheck.Target = new(ir.Package)

    typecheck.NeedITab = func(t, iface *types.Type) { reflectdata.ITabAddr(t, iface) }
    typecheck.NeedRuntimeType = reflectdata.NeedRuntimeType // TODO(rsc): TypeSym for lock?

    base.AutogeneratedPos = base.Ctxt.PosTable.XPos(src.MakePos(src.NewFileBase("<autogenerated>", "<autogenerated>"), 1, 0))

    typecheck.InitUniverse()
}

func dumpIrTree(nodes []ir.Node) {
    for _, n := range nodes {
        s := fmt.Sprintf("\nafter noder2 %v", n)
        ir.Dump(s, n)
    }
}

func TestIrTree(t *testing.T) {
    code := `
package p

import "fmt"

func genericFun[T any](i, j T)  {
    fmt.Printf("i: %v, j: %v\n",i, j)
}

func main() {
   genericFun(5,6)
   genericFun[float32](10.5, 11.8)
}
`
    f, err := parseSrc("generic_testTypecheckConst", code)

    if err != nil {
        panic(err)
    }

    var conf types2.Config
    conf.Trace = false
    conf.Importer = defaultImporter()
    conf.Error = func(err error) {
        fmt.Printf("Typecheck Error: %v\n", err)
    }

    info := types2.Info{
        Types:      make(map[syntax.Expr]types2.TypeAndValue),
        Defs:       make(map[*syntax.Name]types2.Object),
        Uses:       make(map[*syntax.Name]types2.Object),
        Selections: make(map[*syntax.SelectorExpr]*types2.Selection),
        Implicits:  make(map[syntax.Node]types2.Object),
        Scopes:     make(map[syntax.Node]*types2.Scope),
        Inferred:   make(map[syntax.Expr]types2.Inferred),
    }

    pkg, err := conf.Check("<no package>", []*syntax.File{f}, &info)
    if err != nil {
        panic(err)
    }

    var m posMap
    g := irgen{
        target: typecheck.Target,
        self:   pkg,
        info:   &info,
        posMap: m,
        objs:   make(map[types2.Object]*ir.Name),
        typs:   make(map[types2.Type]*types.Type),
    }

    p := &noder{
        err:         make(chan syntax.Error),
        trackScopes: base.Flag.Dwarf,
        file:        f,
    }
    g.generate([]*noder{p})

    dumpIrTree(typecheck.Target.Decls)
}
```

其中 `init` 为精简之后的编译器初始化逻辑， `parseSrc` 用来完成语法分析， `defaultImporter` 用来在类型检查时加载包。运行 `TestIrTree` 将会得到最终的 IR Tree 结构信息。


# 5.7 总结

从更概括的角度上来讲，IR 是编译器内部表示源代码的数据结构，IR 中通常已经抹掉了所有与源程序相关的信息，这样便可以将编译器前端与后端完全解耦。但 Go 编译器只是为了编译 Go 语言，所以 IR 也是围绕着 Go 语言来设计的，例如在 IR Tree 中存在`OMAKECHAN`这种 Go 语言才会有的 Op。

IR Tree 是编译器后续步骤的基础，从现在开始，后面的所有步骤都是基于 IR Tree 来展开了。


# 6.1 简介

我们知道 Go 语言运行时，首先会自动对所加载的包进行初始化，按照执行顺序，一个包的初始化任务可以分为如下三类：

1. 依赖包的初始化逻辑 如果当前包依赖了其它的包，那么这些包的初始化逻辑需要首先完成，也可以看着是本包初始化任务的一部分
2. &#x20;全局变量的赋值语句 全局变量、全局常量的赋值语句也是初始化逻辑的一部分，但常量的初始化工作在类型检查时已经完成，而变量的初始化则稍微复杂一些，例如如下代码：

   ```
   var nameAlias string = name + "!"
   var name string = func() string {
       return "Golang"
   }()

   const version = "1.2"

   var versionAlias string = version
   ```

   变量 `nameAlias` 依赖于 `name` ，因此其初始化顺序应该在 `name` 之后，而 `name` 的初始化需要执行一个函数，这需要在程序运行时才能完成；再看 `versionAlias`, 其初始化逻辑只是简单地拷贝常量 `version` 的值，而后者在编译时便可以确定，所以看起来 `versionAlias` 的初始化工作可以在编译时完成，以提升程序运行时的性能。

   &#x20;由此可见，对于全局变量的赋值语句，编译器需要确定其初始化时间（编译时或运行时）、依赖关系以及初始化的顺序。
3. `init` 函数 由 `init` 函数定义包的初始化逻辑会在包加载时自动执行，并且一个包内可以申明任意多个 `init` 函数，其执行顺序与文件中的申明顺序一致

&#x20;编译器会将以上三类初始化逻辑合并，并创建一个可执行的初始化任务，以便程序执行时调用。


# 6.2 代码结构

在编译器主函数内，处理初始化任务的代码紧跟着语法分析与类型检查之后，其代码片段如下：

```go
// file: cmd/compile/internal/gc/main.go

func Main() {
    // 忽略之前代码

    // 语法分析与类型检查
    noder.LoadPackage(flag.Args())

    // 忽略不相关代码

    // 处理初始化任务
    if initTask := pkginit.Task(); initTask != nil {
        typecheck.Export(initTask)
    }

    // 忽略之后代码
}
```

&#x20;处理初始化任务的代码位于包 `pkginit` 与 `staticinit` 中，总共涉及到三个文件：

* `cmd/compile/internal/pkginit/init.go` \
  处理初始化代码的主逻辑，即上面代码中的函数 `pkginit.Task()` 即在该文件中
* `cmd/compile/internal/pkginit/initorder.go` \
  处理全局变量赋值语句，根据其相互关系确定初始化顺序
* `cmd/compile/internal/staticinit/sched.go` \
  判断赋值语句是动态执行（运行时）还是静态执行（编译时），如果是后者则完成初始化操作


# 6.3 总体逻辑

创建初始化任务的主逻辑在文件`cmd/compile/internal/pkginit/init.go`的方法`Task()`中，刨去细节处理，总体逻辑可以整理如下：

```go
func Task() *ir.Name {
	// Step 1: 处理 import 语句，按顺序查找出所有依赖包中的初始化任务
	var deps []*obj.LSym
	for _, pkg := range typecheck.Target.Imports {
		n := typecheck.Resolve(ir.NewIdent(base.Pos, pkg.Lookup(".inittask")))
		deps = append(deps, n.(*ir.Name).Linksym())
	}

	// Step 2: 处理全局变量赋值语句，函数 initOrder 用来确定初始化顺序，并且返回需要在运行时初始化的赋值语句
	nf := initOrder(typecheck.Target.Decls)
	var fns []*obj.LSym
	if len(nf) > 0 {
		// 如果有需要动态执行的赋值语句，则创建一个 init 函数，并将该函数的函数体设置为所有的动态赋值语句
		// 该 init 函数在所有用户自定义的 init 函数之前
		initializers := typecheck.Lookup("init")
		fn := typecheck.DeclFunc(initializers, ir.NewFuncType(base.Pos, nil, nil, nil))
		fn.Body = nf
		typecheck.Target.Decls = append(typecheck.Target.Decls, fn)
		fns = append(fns, fn.Linksym())
	}

	// Step 3: 处理用户申明的 init 函数，按照顺序添加到 fns 中
	for _, fn := range typecheck.Target.Inits {
		// 代码优化，去除函数中的无效代码，如果优化之后函数体为空，则忽略该 init 函数
		deadcode.Func(fn)

		if len(fn.Body) == 1 {
			if stmt := fn.Body[0]; stmt.Op() == ir.OBLOCK && len(stmt.(*ir.BlockStmt).List) == 0 {
				continue
			}
		}
		fns = append(fns, fn.Nname.Linksym())
	}

	// Step 4: 创建初始化任务 .inittask, 依次写入 deps 及 fns
	sym := typecheck.Lookup(".inittask")
	task := typecheck.NewName(sym)
	task.Class = ir.PEXTERN
	sym.Def = task
	lsym := task.Linksym()
	ot := 0
	ot = objw.Uintptr(lsym, ot, 0) // state: not initialized yet
	ot = objw.Uintptr(lsym, ot, uint64(len(deps)))
	ot = objw.Uintptr(lsym, ot, uint64(len(fns)))
	for _, d := range deps {
		ot = objw.SymPtr(lsym, ot, d, 0)
	}
	for _, f := range fns {
		ot = objw.SymPtr(lsym, ot, f, 0)
	}
	return task
}
```

Step 4 的代码细节不用太在意，重点是我们知道其最终创建了一个名叫`.inittask`的符号对象，并依次将前面三步的初始化内容写了进去。回顾[代码结构](/6.-golang-bian-yi-qi-chu-shi-hua-ren-wu/6.2-dai-ma-jie-gou), 编译器最终将`.inittask`设置为 Export 符号，并最终在编译的最后阶段将该符号对象写入对象文件（.o 或者 .a 文件）。所以在 Step 1 中可以通过该名字查找到其他依赖包中的初始化任务。

接下来，我们详细探讨一下 Step 2 中对全局变量的赋值语句的处理逻辑。


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


# 6.5 编译日志

在[Schedule.StaticInit()](/6.-golang-bian-yi-qi-chu-shi-hua-ren-wu/6.4-fu-zhi-yu-ju#chu-shi-hua-shi-jian)方法中我们可以看到`base.Flag.Percent`用来控制是否 dump 动态初始化语句，该 Flag 对应的编译参数是`-%`. 将如下代码保存至文件`main.go` 中：

```go
package main

import "unsafe"

// 静态初始化语句
var version = 1.2
var versionAlias = version
var names = []string{"Goalng"}
var nameBytes []byte = []byte("Golang")
var nameSize = unsafe.Sizeof("Golang!") // 此处 unsafe.Sizeof 的代码在编译时执行

// 动态初始化语句
var nextVersion = version + 1
var firstName = names[0]
```

通过命令编译文件`go tool compile -G=3 -%=1 main.go`会得到如下结果：

> ```
> nonstatic [0xc000111d60]
> .   AS tc(1) # main.go:13
> .   .   NAME-main.nextVersion tc(1) Class:PEXTERN Offset:0 float64 # main.go:13
> .   .   ADD tc(1) float64 # main.go:13 float64
> .   .   .   NAME-main.version tc(1) Class:PEXTERN Offset:0 float64 # main.go:7
> .   .   .   LITERAL-1 tc(1) float64 # main.go:13
> nonstatic [0xc000111e50]
> .   AS tc(1) # main.go:14
> .   .   NAME-main.firstName tc(1) Class:PEXTERN Offset:0 string # main.go:14
> .   .   INDEX tc(1) string # main.go:14 string
> .   .   .   NAME-main.names tc(1) Class:PEXTERN Offset:0 SLICE-[]string # main.go:9
> .   .   .   LITERAL-0 tc(1) int # main.go:14
> ```

可以看出`nextVersion`与`firstName`两个赋值语句的初始化需要在运行时进行。


# 6.6 Unit Test

初始化的 UT 可以参考 [IR Tree](/golang-bian-yi-qi-ir-tree/5.6-unit-test) 一章, 因为初始化工作本身就需要基于 IR Tree, 所以要调试这段逻辑的话，将如下语句加入到前者 UT 的末尾，然后运行测试用例即可：

```
pkginit.Task()
```


# 6.7 总结

通过这一章的分析我们知道，每个包都有一个初始化任务`.inittask`, 该初始化任务会在程序运行时被执行。另外我们也看到 Go 尽可能地在编译时完成更多的事情，以便提升运行时性能。在后续章节中，我们将专门讨论更多与平台无关的优化策略。


# 7.1 简介

程序中可能存在一些代码，虽然具备语义上的价值，但程序在运行时可能永远不会执行到。例如下列代码：

```go
package main

const version = 1.16 // 可能定义在某个第三方包中的常量

func main() {
    if version <= 1.15 {
        // Do something
    } else {
        // Do something else
    }
}
```

可以发现上述 if 语句只会执行 `else` 中的代码，因此 `if body` 中的代码可以完全删除。除了分支（Branch）语句，逻辑运算符也可能因为短路效应（short-circuit）而造成始终只执行部分代码，例如：

```go
package main

const version = 1.16 // 可能定义在某个第三方包中的常量

func main() {
    correctVersion := version <= 1.15 && version >= 1.14
}
```

其中第一个分量 `version <= 1.15` 为 false, 因此整个逻辑表达式的值也是 false, 没有必要再对第二个分量求值，代码 `version >= 1.14` 为无效代码。

聪明的编译器应该有能力清除掉这样的无效代码，这样可以减小最终可执行文件的大小，并且在程序执行时提升缓存命中率（增加程序的局部性，减少 Page Fault 中断的次数），从而提升程序性能。


# 7.2 处理逻辑

在编译器主函数中，处理无效代码的代码片段如下：

```go
// file: cmd/compile/internal/gc/main.go

func Main() {
    // 忽略之前代码

    // 清除无效代码
    for _, n := range typecheck.Target.Decls {
        if n.Op() == ir.ODCLFUNC {
            deadcode.Func(n.(*ir.Func))
        }
    }

    // 忽略之后代码
}
```

对于所有的函数（包括方法），编译器将通过函数 `deadcode.Func()` 清除其无效代码。所有清除无效代码的逻辑都定义在文件 `cmd/compile/internal/deadcode/deadcode.go` 中，该文件总共只有100多行，逻辑相对简单。总体思路是遍历函数的语法树（IR Tree），然后对分支（Branch）及逻辑运算表达式进行判定，如果确实有可以清除掉的无效代码，则直接修改语法树。

我们先看一下其对 `if` 语句的处理：

```go
if n.Op() == ir.OIF {
    n := n.(*ir.IfStmt)
    n.Cond = expr(n.Cond)
    if ir.IsConst(n.Cond, constant.Bool) {
        var body ir.Nodes
        if ir.BoolVal(n.Cond) {
            n.Else = ir.Nodes{}
            body = n.Body
        } else {
            n.Body = ir.Nodes{}
            body = n.Else
        }
    }
}
```

如果 `if` 的条件语句的值是常量，则根据常量值在 `Body` 与 `Else` 中二选一。逻辑运算符的 `&&` 与 `||` 的处理逻辑如下：

```go
func expr(n ir.Node) ir.Node {
    // Perform dead-code elimination on short-circuited boolean
    // expressions involving constants with the intent of
    // producing a constant 'if' condition.
    switch n.Op() {
    case ir.OANDAND:
        n := n.(*ir.LogicalExpr)
        n.X = expr(n.X)
        n.Y = expr(n.Y)
        if ir.IsConst(n.X, constant.Bool) {
            if ir.BoolVal(n.X) {
                return n.Y // true && x => x
            } else {
                return n.X // false && x => false
            }
        }
    case ir.OOROR:
        n := n.(*ir.LogicalExpr)
        n.X = expr(n.X)
        n.Y = expr(n.Y)
        if ir.IsConst(n.X, constant.Bool) {
            if ir.BoolVal(n.X) {
                return n.X // true || x => true
            } else {
                return n.Y // false || x => x
            }
        }
    }
    return n
}
```

如果第一个分量是常量，那么编译器会根据常量值从两个分量中二选一，用来替换掉之前的整个逻辑表达式。


# 7.3 Unit Test

初始化的 UT 可以参考 IR Tree, 因为初始化工作本身就需要基于 IR Tree. 我们查看一下如下代码的处理结果：

```go
package p

const version = 1.16

func main() {
    if version <= 1.15 {
        println("Inside Body")
    } else {
        println("Inside Else")
    }
}
```

此处的 `if body` 应该从 IR Tree 中清除掉。为了方便，这里我们将所有的测试代码拷贝到测试文件 `cmd/compile/internal/noder/deadcode_test.go`, 完整内容如下：

```go
package noder

import (
    "bufio"
    "cmd/compile/internal/amd64"
    "cmd/compile/internal/base"
    "cmd/compile/internal/deadcode"
    "cmd/compile/internal/escape"
    "cmd/compile/internal/inline"
    "cmd/compile/internal/ir"
    "cmd/compile/internal/reflectdata"
    "cmd/compile/internal/ssa"
    "cmd/compile/internal/ssagen"
    "cmd/compile/internal/syntax"
    "cmd/compile/internal/typecheck"
    "cmd/compile/internal/types"
    "cmd/compile/internal/types2"
    "cmd/internal/obj"
    "cmd/internal/objabi"
    "cmd/internal/src"
    "fmt"
    "os"
    "strings"
    "testing"
)

func parseSrc(path, src string) (*syntax.File, error) {
    var mode syntax.Mode
    mode = syntax.AllowGenerics | syntax.CheckBranches
    errh := func(error) {} // dummy error handler so that parsing continues in presence of errors
    return syntax.Parse(syntax.NewFileBase(path), strings.NewReader(src), errh, nil, mode)
}

func defaultImporter() *gcimports {
    return &gcimports{
        packages: map[string]*types2.Package{},
    }
}

func init() {
    amd64.Init(&ssagen.Arch)

    base.Ctxt = obj.Linknew(ssagen.Arch.LinkArch)
    base.Ctxt.DiagFunc = base.Errorf
    base.Ctxt.DiagFlush = base.FlushErrors
    base.Ctxt.Bso = bufio.NewWriter(os.Stdout)

    base.Ctxt.UseBASEntries = base.Ctxt.Headtype != objabi.Hdarwin

    types.LocalPkg = types.NewPkg("", "")
    types.LocalPkg.Prefix = "\"\""

    types.LocalPkg.Height = types.MaxPkgHeight

    types.BuiltinPkg = types.NewPkg("go.builtin", "") // TODO(gri) name this package go.builtin?
    types.BuiltinPkg.Prefix = "go.builtin"            // not go%2ebuiltin

    // pseudo-package, accessed by import "unsafe"
    ir.Pkgs.Unsafe = types.NewPkg("unsafe", "unsafe")

    ir.Pkgs.Runtime = types.NewPkg("go.runtime", "runtime")
    ir.Pkgs.Runtime.Prefix = "runtime"

    // pseudo-packages used in symbol tables
    ir.Pkgs.Itab = types.NewPkg("go.itab", "go.itab")
    ir.Pkgs.Itab.Prefix = "go.itab" // not go%2eitab

    // pseudo-package used for methods with anonymous receivers
    ir.Pkgs.Go = types.NewPkg("go", "")

    base.DebugSSA = ssa.PhaseOption

    ssagen.Arch.LinkArch.Init(base.Ctxt)
    ir.EscFmt = escape.Fmt
    ir.IsIntrinsicCall = ssagen.IsIntrinsicCall
    inline.SSADumpInline = ssagen.DumpInline
    ssagen.InitEnv()
    ssagen.InitTables()

    types.PtrSize = ssagen.Arch.LinkArch.PtrSize
    types.RegSize = ssagen.Arch.LinkArch.RegSize
    types.MaxWidth = ssagen.Arch.MAXWIDTH

    typecheck.Target = new(ir.Package)

    typecheck.NeedITab = func(t, iface *types.Type) { reflectdata.ITabAddr(t, iface) }
    typecheck.NeedRuntimeType = reflectdata.NeedRuntimeType // TODO(rsc): TypeSym for lock?

    base.AutogeneratedPos = base.Ctxt.PosTable.XPos(src.MakePos(src.NewFileBase("<autogenerated>", "<autogenerated>"), 1, 0))

    typecheck.InitUniverse()
}

func dumpIrTree(nodes []ir.Node) {
    for _, n := range nodes {
        s := fmt.Sprintf("\nafter noder2 %v", n)
        ir.Dump(s, n)
    }
}

func TestIrTree(t *testing.T) {
    code := `
package p

const version = 1.16

func main() {
if version <= 1.15 {
println("Inside Body")
} else {
println("Inside Else")
}
}
`
    f, err := parseSrc("generic_testTypecheckConst", code)

    if err != nil {
        panic(err)
    }

    var conf types2.Config
    conf.Trace = false
    conf.Importer = defaultImporter()
    conf.Error = func(err error) {
        fmt.Printf("Typecheck Error: %v\n", err)
    }

    info := types2.Info{
        Types:      make(map[syntax.Expr]types2.TypeAndValue),
        Defs:       make(map[*syntax.Name]types2.Object),
        Uses:       make(map[*syntax.Name]types2.Object),
        Selections: make(map[*syntax.SelectorExpr]*types2.Selection),
        Implicits:  make(map[syntax.Node]types2.Object),
        Scopes:     make(map[syntax.Node]*types2.Scope),
        Inferred:   make(map[syntax.Expr]types2.Inferred),
    }

    pkg, err := conf.Check("<no package>", []*syntax.File{f}, &info)
    if err != nil {
        panic(err)
    }

    var m posMap
    g := irgen{
        target: typecheck.Target,
        self:   pkg,
        info:   &info,
        posMap: m,
        objs:   make(map[types2.Object]*ir.Name),
        typs:   make(map[types2.Type]*types.Type),
    }

    p := &noder{
        err:         make(chan syntax.Error),
        trackScopes: base.Flag.Dwarf,
        file:        f,
    }
    g.generate([]*noder{p})

    println("Before Deadcode:")
    dumpIrTree(typecheck.Target.Decls)

    for _, n := range typecheck.Target.Decls {
        if n.Op() == ir.ODCLFUNC {
            deadcode.Func(n.(*ir.Func))
        }
    }

    println("After Deadcode:")
    dumpIrTree(typecheck.Target.Decls)
}
```

如果只看 `main` 函数的 `body` 的话，前后的 IR Tree 对比如下：

&#x20;清理无效代码前：

> ```
> .   DCLFUNC-body
> .   .   IF # deadcode:27
> .   .   IF-Cond
> .   .   .   LITERAL-false tc(1) bool # deadcode:27
> .   .   IF-Body
> .   .   .   PRINTN tc(1) Use:3 # deadcode:28
> .   .   .   PRINTN-Args
> .   .   .   .   LITERAL-“Inside Body” tc(1) string # deadcode:28
> .   .   IF-Else
> .   .   .   PRINTN tc(1) Use:3 # deadcode:30
> .   .   .   PRINTN-Args
> .   .   .   .   LITERAL-“Inside Else” tc(1) string # deadcode:30
> ```

清理无效代码后：

> ```
> .   DCLFUNC-body
> .   .   IF # deadcode:27
> .   .   IF-Cond
> .   .   .   LITERAL-false tc(1) bool # deadcode:27
> .   .   IF-Else
> .   .   .   PRINTN tc(1) Use:3 # deadcode:30
> .   .   .   PRINTN-Args
> .   .   .   .   LITERAL-“Inside Else” tc(1) string # deadcode:30
> ```

后者已经没有了 `IF-Body` 这个节点。


# 8.1 简介

函数调用的上下文切换存在固定的额外开销，如果程序中存在大量的函数调用，则势必会影响程序的执行性能。而上下文切换的开销是基本固定的，所以相对于函数的总体执行开销，小函数上下文切换所占的比重更大。因此减少程序中的小函数调用，是优化程序性能的重要方向。

Inline(内联)便是这样的一种技术，其用函数体替换掉函数调用，例如如下代码：

```go
func sum(base, i int) int {
    return 100/base + i
}

func doSum(base int, ints []int) int {
    var s int
    for _, val := range ints {
        s += sum(base, val)
    }

    return s
}
```

Inline 之后代码为：

```go
func doSum(base int, ints []int) int {
    var s int
    for _, val := range ints {
        s += 100/base + val
    }

    return s
}
```

这样我们就去掉了所有对函数 `sum` 的调用。不仅如此，编译器还可以对 Inline 之后的代码做进一步的优化，例如上述代码中， `100/base` 在每次循环中都不会改变，因此编译器可以引入一个局部变量做进一步优化：

```go
func doSum(base int, ints []int) int {
    var s int
    baseTemp := 100 / base
    for _, val := range ints {
        s += baseTemp + val
    }

    return s
}
```

这样 `100/base` 只需要计算一次，而不用在每个 for 循环体内重复计算。


# 8.2 Inline的问题

Inline 不会改变函数的行为，但会缩短程序中的函数调用链，这对于错误信息的展示是不友好的。以上述代码为例，当参数 `base=0` 时， 理想状况下程序应该汇报的错误代码位置在函数 `sum()` 内，而非 Inline 之后的 `doSum()` 中。因此编译器在 Inline 时必须维持原先代码的结构，并将其与 Inline 之后的代码关联起来，这会引入额外的复杂度。

另外一点是对某个函数进行 Inline 时，编译器会将所有调用该函数的地方进行替换，这会导致程序体积变大，最终的可执行文件也会变大。因此内联操作并不是越多越好，编译器需要找到一种平衡。


# 8.3 代码结构

对 Go 而言，编译器主函数在清除了无效代码之后，便会进行 Inline 操作，入口代码如下：

```go
// file: cmd/compile/internal/gc/main.go

func Main() {
    // 忽略之前代码

    // 内联操作
    if base.Flag.LowerL != 0 {
        inline.InlinePackage()
    }

    // 忽略之后代码
}
```

如果不想要内联操作，可以通过编译参数 `-l` 来禁止。Inline 的所有代码都在文件 `cmd/compile/internal/inline/inl.go` 中。


# 8.4 处理逻辑

编译器对函数进行内联优化时会涉及到三个方面：&#x20;

1. 如何遍历函数的调用链&#x20;
2. 如何判定一个函数是否可以内联&#x20;
3. 如何完成内联

接下来我们详细介绍内联的实现策略。


# 8.4.1 遍历调用链

函数的调用关系形成了一个有向图（Directed Graph），在对函数进行内联操作时，我们需要反向对函数的调用链进行分析。例如函数的调用链是 `A()->B()-C()`, 则我们需要先分析能否将 C 内联到 B 内，再分析是否能将 B 内联到 A 中。

&#x20;遍历函数的逻辑封装在文件 `cmd/compile/internal/ir/scc.go` 中，该文件内封装了自底向上遍历函数 AST 的方法。SCC 的全称是 [Strongly connected components](https://en.wikipedia.org/wiki/Strongly_connected_component)，一个 SCC 是由有向图的节点构成的子集，其中任意两个节点之间都至少有一条连通的路径。例如下面的有向图内有三个 SCC:

![SCC - 图片来自维基百科](/files/-MapjFm8BLUC3UMUXBGj)

对于函数调用链形成的有向图，该文件中的函数会自底向上找出每个 SCC, 然后依次传递给分析函数进行处理。遍历函数的签名如下：

```go
func VisitFuncsBottomUp(list []Node, analyze func(list []*Func, recursive bool)) {
    // Ignore function body
}
```

对于参数 `list` 中的每个函数节点，函数会将找出的 SCC 依次传递给 `analyze` 函数进行处理。对于如下代码：

```go
func B() {
    println("B")
}

func A() {
    println("A")
    B()
}

func C() {
    println("C")
    D()
}

func D() {
    println("D")
    C()
}

func main() {
    A()
    C()
}
```

整个函数的调用关系图如下：

![Func Call Stack & SCC](/files/-MapjLeBZSdGxZ1TneeU)

每种颜色的函数都形成一个 SCC, 处理的顺序是：{B}, {A}, {C, D}, {main}.


# 8.4.2 内联判断

整个内联功能可以通过编译参数 `-l` 来禁止，对于某个特定函数，也可以通过编译命令 `go:noinline` 来禁止内联。除此之外，还有一些特定的情况下函数不能内联，例如函数体为空，或者函数包含一些特定的编译命令例如 `go:cgo_unsafe_args` 等。

除了这些明确的场景，一个函数能否内联取决于该函数的复杂度，前文中提到，函数调用对小函数影响更大，因此如何衡量函数的大小及复杂度，是编译器需要量化的事情。该逻辑由结构体 `hairyVisitor` 驱动：

```go
type hairyVisitor struct {
    // 内联“预算”，起始值为 80, 由常量 inlineMaxBudget 定义，函数体内各种操作都会有对应的“代价（cost）”，总体“代价”超过“预算”的话，那么该函数就会由于太复杂而无法内联
    budget int32
    // 保存不能内联的理由
    reason string
    // 方法调用的开销，默认为 57, 由常量 inlineExtraCallCost 定义
    extraCallCost int32
    // 用来记录当前函数的局部变量，函数内联时需要使用
    usedLocals ir.NameSet
    // 遍历函数并进行复杂度计算，实际为方法 hairyVisitor.doNode()
    do func(ir.Node) bool
}
```

编译器定义了一个“内联代价（Inline Cost）”来表达函数复杂度，内联代价与函数 AST 的节点数目正相关，每个子节点的 cost 为 1, 如果该节点还需要额外的操作，则还需要减掉对应的Cost, 例如函数调用的 Cost 是 57, 由常量 `inlineExtraCallCost` 定义；内联总代价小于 80 的函数可以内联，该数字被称为函数的“内联预算（Budget）”，由常量 `inlineMaxBudget` 定义。

计算内联代价的方法是 `hairyVisitor.doNode()`, 该方法对函数的 AST(IR Tree) 进行深度优先（DFS）遍历，并从“内联预算（budget 字段）”中减掉函数体的各种“代价（cost）”，如果最终预算还有盈余，则函数可被内联。例如下列函数：

```go
func B() {
    println("B")
    println("B")
}

// go:noinline
func C() {
    println("C")
}

func A() {
    println("A")
    C()
}
```

对于函数 B，语句 `println("B")` 总体的代价是 2, 因此整个函数的代价便是 4, 该函数没有用完内联预算，因此是可以内联的。而对于函数 A，其内联代价的计算如下：

> &#x20;Cost(A) = 2 + 2 + 57 = 61&#x20;
>
> 2: 语句 `println("A")` 的代价
>
> 2: 语句 `C()` 本身的代价&#x20;
>
> 57: 由于函数 C 无法内联，所以需要减去一个 extraCallCost, 默认为 57

A 仍然可以内联，但同时可以发现，如果 A 调用了两个以上不可内联的函数的话，那么他就会耗尽“预算”而无法内联了。

归总起来，判断函数能否内联的逻辑封装在函数 `CanInline` 中，精简代码之后其总体逻辑如下：

```go
func CanInline(fn *ir.Func) {
    // Part 1: 判断各种特定情况，
    // If marked "go:noinline", don't inline
    if fn.Pragma&ir.Noinline != 0 {
        reason = "marked go:noinline"
        return
    }
    // 省略判断其它各种情况的代码

    // Part 2: 根据函数的大小判断能否内联
    visitor := hairyVisitor{
        budget:        inlineMaxBudget,
        extraCallCost: cc,
    }
    if visitor.tooHairy(fn) {
        reason = visitor.reason
        return
    }

    // Part 3: 对于可以内联的函数，创建内联节点。该节点在后面内联操作时使用
    n.Func.Inl = &ir.Inline{
        Cost: inlineMaxBudget - visitor.budget,
        Dcl:  pruneUnusedAutos(n.Defn.(*ir.Func).Dcl, &visitor),
        Body: inlcopylist(fn.Body),
    }
}
```

注意最后一部分逻辑，对于可以内联的函数， `CanInline` 会为该函数创建一个 `ir.Inline` 对象。


# 8.4.3 内联操作

我们先看一下内联操作的总体逻辑：

```go
func InlinePackage() {
    ir.VisitFuncsBottomUp(typecheck.Target.Decls, func(scc []*ir.Func, recursive bool) {
        numfns := numNonClosures(scc)
        for _, n := range scc {
            // 判断递归调用场景
            if !recursive || numfns > 1 {
                CanInline(n)
            } else {
                if base.Flag.LowerM > 1 {
                    fmt.Printf("%v: cannot inline %v: recursive\n", ir.Line(n), n.Nname)
                }
            }
            InlineCalls(n)
        }
    })
}
```

对于递归调用，只有函数调用自己这种场景是不允许内联的，如果多个函数形成一个调用环，那么编译器依然会尝试着对其进行内联。

函数`InlineCalls()`完成实际的内联操作，给定函数`fn`, 内联操作的步骤如下：&#x20;

1. 遍历函数体的各个语句，尝试对其进行内联。尝试内联的操作由函数`inlnode()`完成&#x20;
2. 如果当前节点可以内联（肯定是一个函数调用），则创建一个内联节点替换当前节点，内联节点是类型`ir.InlinedCallExpr`, 该操作由`mkinlcall`完成&#x20;
3. 对于新创建的内联节点，对其所有语句递归地进行内联操作

回顾[遍历调用链](/8.-golang-bian-yi-qi-inline/8.4.1-bian-li-tiao-yong-lian)中的示例代码：

```go
func B() {
    println("B")
}

func A() {
    println("A")
    B()
}

func C() {
    println("C")
    D()
}

func D() {
    println("D")
    C()
}

func main() {
    A()
    C()
}
```

以及函数调用的SCC：

![Func Call Stack & SCC](/files/-MapjLeBZSdGxZ1TneeU)

我们以`scc = [C, D]`为例来看一下编译器做内联的详细步骤：&#x20;

1. 遍历 scc, 处理节点 C, 函数`CanInline(C)`识别出该函数可以内联，并为其创建内联属性 Inl&#x20;
2. 调用`InlineCalls(C)`对 C 进行内联操作 遍历 C 的函数体，并尝试着对每个语句进行内联：第一个语句`println("C")`不用内联，第二个语句`D()`有可能可以内联，但此时函数 D 还没有经过`CanInline()`的内联检查，因为`内联属性 Inl`为空，不满足内联条件。到此对 C 的内联操作结束，并没有任何实际的内联操作发生&#x20;
3. 遍历 scc, 处理节点 D, 函数`CanInline(D)`识别出该函数可以内联，并为其创建内联属性 Inl.&#x20;
4. 调用`InlineCalls(D)`对 D 进行内联操作 逻辑同第2步，不同的是编译器发现语句`C()`可以进行内联，于是完成内联后函数 D 变为：

```go
func D() {
    println("D")
    // Inlined statements
    println("C")
    D()
}
```

&#x20;    5\. 完成对 scc 的遍历，结束后两个函数的结果为：

```go
func C() {
    println("C")
    D()
}

func D() {
    println("D")
    // Inlined statements
    println("C")
    D()
}
```

由此可见随着自底向上处理完函数的所有 scc, 整个函数内联操作随即完成。


# 8.4.4 编译日志

编译器提供了参数来反馈编译过程中的各种优化：

> ```
> -json string
> version,file for JSON compiler/optimizer detail output
> -m	print optimization decisions
> ```

我们可以通过以上参数来查看编译器所做的各种优化，包括内联在内。将下列代码保存为 `main.go`:

```go
package main

func C() {
    println("C")
    D()
}

func D() {
    println("D")
    C()
}

func main() {
    C()
}
```

通过命令 `go tool compile -G=3 -m -json "0,file:///tmp/main.json" main.go` 进行编译，可以看到如下输出：

> ```
> main.go:3:6: can inline C
> main.go:8:6: can inline D
> main.go:10:3: inlining call to C
> main.go:13:6: can inline main
> main.go:14:3: inlining call to C
> main.go:14:3: inlining call to D
> ```

文件 `/tmp/main.json` 也会包含相关信息。其中 `-m` 对应的数字越大，日志就越详细，例如我们通过命令 `go tool compile -G=3 -m=2 -json "0,file:///tmp/main.json" main.go` 编译时，得到的结果如下：

> ```
> main.go:3:6: can inline C with cost 61 as: func() { println(string(“C”)); D() }
> main.go:8:6: can inline D with cost 65 as: func() { println(string(“D”)); C() }
> main.go:10:3: inlining call to C func() { println(string(“C”)); D() }
> main.go:13:6: can inline main with cost 63 as: func() { C() }
> main.go:14:3: inlining call to C func() { println(string(“C”)); D() }
> main.go:14:3: inlining call to D func() { println(string(“D”)); C() }
> main.go:14:3: cannot inline C into main: repeated recursive cycle
> ```

&#x20;其中 `-m -m` 与 `-m=2` 效果一样，当 `-m=4` 时，日志还会打印出内联前后的 IR Tree 结构，读者可以自己尝试。


# 8.4.5 Unit Test

内联的 UT 可以参考IR Tree, 因为内联操作修改的便是 IR Tree. 例如如果我们想要测试如下代码：

```go
package p

func C() {
    println("C")
    D()
}

func D() {
    println("D")
    C()
}

func main() {
    C()
}
```

为了方便，这里我们将所有的测试代码拷贝到测试文件 `cmd/compile/internal/noder/inl_test.go`, 完整内容如下：

```go
package noder

import (
    "bufio"
    "cmd/compile/internal/amd64"
    "cmd/compile/internal/base"
    "cmd/compile/internal/deadcode"
    "cmd/compile/internal/escape"
    "cmd/compile/internal/inline"
    "cmd/compile/internal/ir"
    "cmd/compile/internal/reflectdata"
    "cmd/compile/internal/ssa"
    "cmd/compile/internal/ssagen"
    "cmd/compile/internal/syntax"
    "cmd/compile/internal/typecheck"
    "cmd/compile/internal/types"
    "cmd/compile/internal/types2"
    "cmd/internal/obj"
    "cmd/internal/objabi"
    "cmd/internal/src"
    "fmt"
    "os"
    "strings"
    "testing"
)

func parseSrc(path, src string) (*syntax.File, error) {
    var mode syntax.Mode
    mode = syntax.AllowGenerics | syntax.CheckBranches
    errh := func(error) {} // dummy error handler so that parsing continues in presence of errors
    return syntax.Parse(syntax.NewFileBase(path), strings.NewReader(src), errh, nil, mode)
}

func defaultImporter() *gcimports {
    return &gcimports{
        packages: map[string]*types2.Package{},
    }
}

func init() {
    amd64.Init(&ssagen.Arch)

    base.Ctxt = obj.Linknew(ssagen.Arch.LinkArch)
    base.Ctxt.DiagFunc = base.Errorf
    base.Ctxt.DiagFlush = base.FlushErrors
    base.Ctxt.Bso = bufio.NewWriter(os.Stdout)

    base.Ctxt.UseBASEntries = base.Ctxt.Headtype != objabi.Hdarwin

    types.LocalPkg = types.NewPkg("", "")
    types.LocalPkg.Prefix = "\"\""

    types.LocalPkg.Height = types.MaxPkgHeight

    types.BuiltinPkg = types.NewPkg("go.builtin", "") // TODO(gri) name this package go.builtin?
    types.BuiltinPkg.Prefix = "go.builtin"            // not go%2ebuiltin

    // pseudo-package, accessed by import "unsafe"
    ir.Pkgs.Unsafe = types.NewPkg("unsafe", "unsafe")

    ir.Pkgs.Runtime = types.NewPkg("go.runtime", "runtime")
    ir.Pkgs.Runtime.Prefix = "runtime"

    // pseudo-packages used in symbol tables
    ir.Pkgs.Itab = types.NewPkg("go.itab", "go.itab")
    ir.Pkgs.Itab.Prefix = "go.itab" // not go%2eitab

    // pseudo-package used for methods with anonymous receivers
    ir.Pkgs.Go = types.NewPkg("go", "")

    base.DebugSSA = ssa.PhaseOption

    ssagen.Arch.LinkArch.Init(base.Ctxt)
    ir.EscFmt = escape.Fmt
    ir.IsIntrinsicCall = ssagen.IsIntrinsicCall
    inline.SSADumpInline = ssagen.DumpInline
    ssagen.InitEnv()
    ssagen.InitTables()

    types.PtrSize = ssagen.Arch.LinkArch.PtrSize
    types.RegSize = ssagen.Arch.LinkArch.RegSize
    types.MaxWidth = ssagen.Arch.MAXWIDTH

    typecheck.Target = new(ir.Package)

    typecheck.NeedITab = func(t, iface *types.Type) { reflectdata.ITabAddr(t, iface) }
    typecheck.NeedRuntimeType = reflectdata.NeedRuntimeType // TODO(rsc): TypeSym for lock?

    base.AutogeneratedPos = base.Ctxt.PosTable.XPos(src.MakePos(src.NewFileBase("<autogenerated>", "<autogenerated>"), 1, 0))

    typecheck.InitUniverse()
}

func dumpIrTree(nodes []ir.Node) {
    for _, n := range nodes {
        s := fmt.Sprintf("\nafter noder2 %v", n)
        ir.Dump(s, n)
    }
}

func TestIrTree(t *testing.T) {
    code := `
package p

func C( )  {
    println("C")
D()
}

func D( )  {
    println("D")
C()
}


func main() {
C()
}
`
    base.Flag.LowerM = 2
    f, err := parseSrc("Inline", code)

    if err != nil {
        panic(err)
    }

    var conf types2.Config
    conf.Trace = false
    conf.Importer = defaultImporter()
    conf.Error = func(err error) {
        fmt.Printf("Typecheck Error: %v\n", err)
    }

    info := types2.Info{
        Types:      make(map[syntax.Expr]types2.TypeAndValue),
        Defs:       make(map[*syntax.Name]types2.Object),
        Uses:       make(map[*syntax.Name]types2.Object),
        Selections: make(map[*syntax.SelectorExpr]*types2.Selection),
        Implicits:  make(map[syntax.Node]types2.Object),
        Scopes:     make(map[syntax.Node]*types2.Scope),
        Inferred:   make(map[syntax.Expr]types2.Inferred),
    }

    pkg, err := conf.Check("<no package>", []*syntax.File{f}, &info)
    if err != nil {
        panic(err)
    }

    var m posMap
    g := irgen{
        target: typecheck.Target,
        self:   pkg,
        info:   &info,
        posMap: m,
        objs:   make(map[types2.Object]*ir.Name),
        typs:   make(map[types2.Type]*types.Type),
    }

    p := &noder{
        err:         make(chan syntax.Error),
        trackScopes: base.Flag.Dwarf,
        file:        f,
    }
    g.generate([]*noder{p})

    println("Before Inline:")
    dumpIrTree(typecheck.Target.Decls)

    inline.InlinePackage()

    println("After Inline:")
    dumpIrTree(typecheck.Target.Decls)
}
```

运行该测试用例，内联后的函数 C 与 D 的结构与前文我们分析的一致：

> ```
> after noder2 C [0xc0001ec2c0]
> .   DCLFUNC tc(1) Iota:-1 ABI:ABIInternal InlinabilityChecked FUNC-func() # Inline:4
> .   DCLFUNC-body
> .   .   PRINTN tc(1) Use:3 # Inline:5
> .   .   PRINTN-Args
> .   .   .   LITERAL-“C” tc(1) string # Inline:5
> .   .   CALLFUNC tc(1) Use:3 # Inline:6
> .   .   .   NAME-p.D tc(1) Class:PFUNC Offset:0 FUNC-func() # Inline:9
>
> after noder2 D [0xc0001ec420]
> .   DCLFUNC tc(1) Iota:-1 Label:1 ABI:ABIInternal InlinabilityChecked FUNC-func() # Inline:9
> .   DCLFUNC-body
> .   .   PRINTN tc(1) Use:3 # Inline:10
> .   .   PRINTN-Args
> .   .   .   LITERAL-“D” tc(1) string # Inline:10
> .   .   BLOCK # Inline:11
> .   .   BLOCK-List
> .   .   .   INLMARK # +Inline:11
> .   .   .   PRINTN tc(1) Use:3 # Inline:5
> .   .   .   PRINTN-Args
> .   .   .   .   LITERAL-“C” tc(1) string # Inline:5
> .   .   .   CALLFUNC tc(1) Use:3 # Inline:6
> .   .   .   .   NAME-p.D tc(1) Class:PFUNC Offset:0 FUNC-func() # Inline:9
> .   .   .   LABEL # Inline:11 p..i0
> ```

读者可以尝试着调试更加复杂的代码，以便查看编译器在内联时如何处理参数以及返回值的，这里不再赘述。


# 8.4.6 总结

深入了解编译器内联机制之后，我们可以写出更加益于编译器优化的代码。例如我们有一个很复杂的函数：

```go
func ReallyBigFunc() {
    // Part 1: Fast Operations with few statements
    // stmt0
    // stmt1
    result := expr()
    if result {
        return
    }

    // Part 2: Slow operations with many statements
    // stmt4
    // stmt5
    // ...
    // stmt100
}
```

假设该函数使用得极其频繁，而大多数情况下函数都会在第一个 `if` 语句处返回，只有极少数情况下需要执行后面的 Slow Operation. 由于函数的总体复杂度很大，因此编译器不会对其进行内联，所以程序在执行时需要付出很大的调用开销。一个优化的策略是让编译器将 Part 1 的代码进行内联，我们可以将 Part 2 单独提取为一个函数：

```go
func slowOp() {
    // Part 2: Slow operations with many statements
    // stmt4
    // stmt5
    // ...
    // stmt100
}

func ReallyBigFunc() {
    // Part 1: Fast Operations with few statements
    // stmt0
    // stmt1
    result := expr()
    if !result {
        slowOp()
    }
}

func main() {
    ReallyBigFunc()
}
```

这样函数 `ReallyBigFunc()` 的行为不会改变但复杂度降低，对于每个调用函数 `ReallyBigFunc()` 的地方编译器都可以将其内联。例如上面的 `main` 函数最终会是如下样子：

```go
func main() {
    // Part 1: Fast Operations with few statements
    // stmt0
    // stmt1
    result := expr()
    if !result {
        slowOp()
    }
}
```

&#x20;这样就极大地避免了原函数的调用开销。


# 9.1 什么是逃逸分析

程序运行时将所使用的内存划分成两部分：

1. &#x20;栈（Stack） 函数的局部内存空间，用于保存函数的局部变量，也包括函数调用时的参数、返回值。栈内存与函数的生命周期完全一致，在函数调用时创建、退出时销毁，由程序自动管理，并且栈上数据仅对当前函数可见，因此，只要内存使用的生命周期在函数生命周期之内，都可以分配栈上的空间。

   &#x20;栈更加高效与安全，但对于使用场景有严格限制。
2. &#x20;堆（Heap） 全局内存空间，由整个程序共享。堆内存的分配与释放需要单独管理，像C语言有专门的分配与释放内存的内置函数，但现在几乎所有的高级编程语言都提供了自动的内存管理策略，Go 语言便是其中之一。由于堆内存的全局可见性，还需要保证并发访问下数据的同步性。

   &#x20;堆更加灵活，但面临着更复杂的管理问题，为程序带来了额外的开销。

由此可见，对于一个内存区域，它在程序中的可见范围决定了应该使用哪种内存：如果该内存区域的可见范围跨越了多个函数，那么就必须使用堆内存，而如果其可见范围仅仅局限于一个函数之内，则可以优先考虑栈空间。

程序的变量就是对内存区域的抽象，因此，对变量的可见范围（作用域）进行分析，进而判断应该将其分配到栈上还是堆上的过程，叫着逃逸分析。即为了简化内存管理，我们默认所有变量都使用栈空间，但如果一个变量由于被多个函数所引用，必须将其转移到堆上，我们就说该变量“逃逸”到了堆上。


# 9.2 Go 的逃逸分析

全局变量的内存区域毫无疑问需要分配到堆上，我们仅对函数的局部变量进行分析。

我们知道 Go 语言遵循 copy-by-value(按值拷贝) 规则, 即程序在赋值、传递函数的参数、以及返回值的过程中，会完全拷贝一份对应类型的数据，然后将拷贝传递给对方。例如如下代码：

```go
type Lang struct {
    Name string
}

func copyLang(arg Lang) (result Lang) {
    l := arg          // 以 arg 的内容重新创建一个 Lang 实例，并将新实例赋值给 l
    l.Name = "Golang" // 修改 l.Name 的属性不会改变 arg 中的内容

    defer func() {
        fmt.Printf("In copyLang: arg.Name: %s, l.Name: %s, result.Name: %s. Add: %p\n", arg.Name, l.Name, result.Name, &result)
    }()

    return l // 同理，此处复制一份 l 赋值给返回值 result
}

func main() {
    l1 := Lang{"Java"}

    fmt.Printf("In main before copyLang: l1.Name: %s\n", l1.Name)
    // 1. 此处复制一份 l1, 然后作为参数传递给函数 copyLang, 即使函数修改了参数的内部属性，也不会影响 l1
    // 2. 将函数 copyLang 的返回值变量 result 复制一份，赋值给 l3
    l3 := copyLang(l1)

    fmt.Printf("In main after copyLang: l1.Name: %s, l3.Name: %s\n", l1.Name, l3.Name)
}
```

特别指出的是：在 `l3 := copyLang(l1)` 语句中，函数的返回值变量 `result` 与 l3 也指向不同的内存区域。对于没有命名的函数返回值，编译器会为其生成一个内部变量名，例如对于代码：

```go
func copyLang(arg Lang) Lang {
    return arg
}
```

编译器处理之后的代码相当于：

```go
func copyLang(arg Lang) (r0 Lang) {
    return arg
}
```

因此效果是一样的。可以发现由于规则 copy-by-value, 上述代码中所涉及的所有局部变量都指向自己单独的内存区域。既然如此，那么局部变量怎么可能需要逃逸呢？答案是指针！例如如下代码：

```go
func copyLang(arg Lang) *Lang {
    l2 := arg

    return &l2
}
```

因为函数返回的是 l2 的指针，所以程序在拷贝时必须将其内存分配到堆上，否则会造成悬挂指针（[Dangling Pointer](https://en.wikipedia.org/wiki/Dangling_pointer)）问题。因此 Go 的逃逸分析都是围绕着指针展开的，其核心思路基于如下两点：

1. 指向栈上对象的指针不能分配到堆上
2. 指向栈上对象的指针，其生命周期不能长于该对象

换而言之，如果一个指针被分配到了堆上，那么其指向的对象一定要分配到堆上；如果一个指针的生命周期长于其指向的对象，那么该对象一定要分配到堆上。上例中返回值指针的生命周期长于 l2, 因此 l2 会逃逸到堆上。

&#x20;除此之外，为了避免栈过于庞大，编译器会直接将大对象分配到堆上。

> &#x20;当函数返回一个指针时，程序也会根据 copy-by-value 规则拷贝一个新的指针对象并返回，但这两个指针对象所指向的是同一块内存地址。




---

[Next Page](/llms-full.txt/1)

