1.6 编译器
编译器本质上是一种翻译器,其作用是将一种高级编译型语言翻译为计算机可以识别的机器语言(即低级语言)。编译器有本地编译器和交叉编译器之分。本地编译器用来生成编译器所在的计算机和操作系统相同的平台环境下可执行的目标代码。交叉编译器可以用来生成在其他平台上可执行的目标代码。
现代编译器都是分层架构,分层能够增加层之间的独立性,从而更好地完成任务。Go编译器分为前端和后端。Go编译器的可执行代码在cmd/compile目录中。Go编译器的编译过程主要分为如下几个阶段:词法解析和语法解析、类型检查、生成SSA中间代码、生成机器代码。
1.6.1 词法分析和语法分析
Go语言的编译器根据编译的参数,对当前涉及的所有Go源代码文件进行词法分析。词法分析的作用就是将源代码文件中的字符串序列化成Token(标记)序列,便于编译器后面的进一步处理和分析。一般来说,我们把执行词法分析的程序称为词法分析器(Lexer)。
Go语法分析过程就是将词法分析生成的Token序列按照Go语言定义好的语法(Grammar)进行整理,从而构建出抽象语法树(AST)。每一个AST都对应着一个单独的Go源代码文件,这个AST中包括当前文件所属的包名、定义的常量、结构体和函数等信息,方便后续调试。
1.6.2 类型检查
Go作为一门静态类型语言,Go源代码文件经过编译器的词法分析和语法分析会生成多个文件的AST。Go语言的编译器会对AST中定义和使用的类型信息进行检查。类型检查的主要内容有:
· 常量、类型和函数名和函数类型。
· 变量的赋值和初始化。
· 函数和闭包的主体。
· 哈希键值对的类型。
· 导入函数体。
· 外部的声明。
通过遍历所有AST语法树上的节点,实现对每一个AST节点上的类型进行验证。在这个过程中,只要有一处类型错误或不匹配,编译器就会终止编译过程,并抛出错误信息。
注意
类型检查这个阶段除了类型验证外,还会对AST进行细节优化,比如无用代码消除、函数调用内联化等。
1.6.3 生成SSA中间代码
Go编译器对AST每个节点上的类型检查通过后,就可以认为Go源代码没有语法错误,此时Go编译器可以将分析得到的AST翻译成中间代码。
Go编译器利用SSA(Static Single Assignment Form)特性来生成中间代码(Go汇编语言)。SSA能够比较容易地分析出代码中的无用变量和片段,并对代码进行优化。
Go汇编语言作为一种中间代码,可以更好地实现Go语言的跨平台运行。Go汇编语言基于Plan 9汇编,并不直接体现机器底层的硬件特性,直到最后续才会根据编译的参数来决定把Go汇编语言翻译成具体平台的机器代码。
1.6.4 生成机器代码
要执行Go程序代码,必须借助Go编译器来生成对应的二进制可执行文件。Go编译器会针对特定硬件架构把中间代码转换成机器代码。该编译过程同样会进行代码优化,例如将变量移动到更靠近它们被使用的位置、删除从未被读取的局部变量以及分配寄存器。
Go目录cmd/compile/internal中包含了非常多的用于生成不同机器代码所需的包。不同类型的CPU(AMD64、ARM、ARM64、MIPS、MIPS 64、ppc64、s390x、x86和WASM)会用特定的包来生成机器代码,这也是Go语言能够在上述不同指令集的CPU上运行的原因。
其中,WASM(Web Assembly)是一种在栈虚拟机上使用的二进制指令格式,使得它成为Web浏览器上具有高可移植性的目标语言。Go语言的编译器可以通过参数GOARCH=wasm GOOS=js来生成WASM格式的可执行目标文件,该可执行文件能够运行在主流的浏览器中。
注意
Go编译器的编译过程在不同版本中存在差异,而且实际过程往往更加复杂。
综上所述,Go编译器的编译过程大致如图1.13所示。
图1.13 Go编译器编译过程示意图