- https://github.com/chai2010/go-ast-book?hmsr=codercto.com&utm_medium=codercto.com&utm_source=codercto.com
- https://www.sohu.com/a/293962794_99930294
- https://www.kancloud.cn/cfun_good/golang/2033481
AST全称Abstract Syntax Tree抽象语法树,即以树状形式表现变成语言的语法结构,树上每个节点都表示源代码中一种结构。之所以说语法是抽象的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
Golang语法树是Go源代码的另一种语义等价的表现形式,Go自带的go fmt
、go doc
等命令都是在Go语法树基础之上的分析工具,将Go语言程序作为输入数据,从语法树的维度重新审视Go语言程序。
$ go run
Golang的go run
命令会完成源代码从编译到执行的过程,简单来说可将go run
等价于go build
+ 执行。
$ go build
参数 | 描述 |
---|---|
go build -n | 不执行地打印流程中用的命令 |
go build -x | 执行并打印流程中使用到的命令 |
go build -work | 打印编译时的临时目录路径,结束时保留,默认编译结束会删除。 |
Golang中go build
主要完成源码的编译和可执行文件的生成。go build
接收参数为.go
文件或目录,默认情况下编译当前目录下所有的.go
文件。在main
包下执行会生成相应的可执行文件,在非main
包下会做一些检查,生成的库文件放在缓存目录下,在工作目录下并无新文件生成。
编译过程
Golang是需要编译才能执行的语言,代码在运行前需要通过编译器生成二进制机器码,随后二进制机器码才能在目标机器上运行。
Golang编码程序首先经过编译器生成plan9
汇编,再由汇编器和链接处理得到最终的可执行程序。
Golang编译器的源代码位于cmd/compile
目录下,目录下的文件共同构成了Golang的编译器,编译器分为前端和后端,前端承担着词法分析、语法分析、类型检查、中间代码生成的工作,后端主要负责目标代码的生成和优化,就是将中间代码翻译成机器能够运行的机器码。
源代码从编译到执行的过程会经过:源码->编译->可执行文件->执行输出
Golang的编译器对Golang源代码的处理在逻辑上可分为四个阶段
1.词法语法分析
2.类型检查和AST转换
3.SSA优化和降级转换
4.Go源码生成对应的plan9
汇编
Golang程序的编译入口是compile/internal/gc/main.go
文件的Main
函数,Main
函数获取命令行参数并更新编译选项和配置,然后运行parseFiles
函数对输入的所有文件进行词法和语法分析,得到对应的AST抽象语法树。
第一阶段:词法和语法分析
编译过程是从解析代码的源文件开始,词法分析的作用是解析源代码文件,将文件中的字符串序列转换为Token序列,以方便后续的处理和解析,一般会把执行词法分析的程序称为词法解析器(lexer)。
顺序 | 阶段 | 工具 | 描述 |
---|---|---|---|
1 | 词法分析 | 词法分析器 | 源代码被token 化 |
2 | 语法分析 | 解析器 | 解析 |
3 | 生成语法树 | 抽象语法树 | 为每个源构造语法树文件 |
1.词法分析
- 词法分析是将字符串转换为标记(Token)序列的过程
词法分析的输入是词法分析器输出的Token序列,Token序列会按顺序被语法分析器进行解析,语法的解析过程是将词法分析生成的Token按照语言定义好的文法(Grammar)自下而上或自上而下的进行规约,每个Go的源代码文件最终会被归纳成一个SourceFile结构。
1SourceFile = PackageClause ";" {ImportDecl";"} {TopLevelDecl";"}
源代码被词法分析器Token
化后进行词法分析,解析器解析后进行语法分析,最后为每个源构造语法树文件。每个语法树都由与之对应的源文件上元素的节点,比如表达式、声明、陈述。
"json.go":SourceFile {
PackageName:"json",
ImportDecl: []Import{"io"},
TopLevelDecl:...
}
Golang中compile/internal/syntax/tokens.go
文件定义了Golang支持的全部token
类型,比如名称和文本、操作符、定界符、关键字等。词法分析会将文本中的字符序列转换为标记序列,比如将关键字package
转换为_package
标记,fun
转换为_fun
标记等。
Golang的词法解析是通过src/cmd/compile/internal/syntax/scanner.go
文件中的syntax.scanner
结构体实现的,由syntax.scanner.next
方法驱动。
src/cmd/compile/internal/syntax/scanner.go
文件实现了词法解析器,使用scanner
结构的next
方法实现.go
文件的扫描并转换为token
序列。next
方法获取文件中未被解析的字符,进入switch/case
分支进行词法解析。
2.语法分析
- 语法分析是根据某种特定的形式文法(Grammer)对Token序列构成的输入文本进行分析并确定其语法结构的过程。
标准的Golang语法分析器使用的是LALR的文法,语法解析的结果是抽象语法树,每个AST都对应着一个单独的Golang文件,这个抽象语法树包括当前文件属于的包名、定义的常量、结构体、函数等。
语法分析会通过文法分析,构建输入的token
序列的语法结构,得到对应的语法树。
如果在语法解析过程中发生了任何语法错误,都会被语法解析器发现并将消息打印到标准输出上,整个编译过程也会随着错误的出现而被中止。
语法解析过程会调用goroutine
,语法解析器位于cmd/compile/internal/syntax.parser
,其中syntax.parser.fileOrNil
为一个核心解析方法。解析的产物是文件对应的抽象语法树。
3.抽象语法树
抽象语法树(AST)是源代码语法结构一种抽象的表示,抽象语法树使用树状的方式表示编程语言中的语法结构。抽象语法树中每个节点表示源代码的一个元素,每个子树表示一个语法元素。
作为编译器中常用的数据结构,抽象语法树会抹去源代码中不重要的字符,比如空格、分号、括号等。编译器在执行完语法分析后会输出一个抽象语法树,抽象语法树会辅助编译器进行语义分析,以此来确定结构正确的程序是否存在类型不匹配或不一致的问题。
第二阶段:语义分析
语义分析主要包括对抽象语法树(AST)进行类型检查和变换
语义分析过程中重要的操作:逃逸分析、变量捕获、函数内联、闭包处理
1. 类型检查
- 通过名称解析和类型推断以确定对象所属的标识符,以及每个表达式具有的类型。
- 类型检查包括某些额外的检查,比如“声明和未使用”以及确定函数是否终止。
类型检查
当拿到一组文件的抽象语法树之后,Golang的编译器会对语法树中定义和使用的类型进行检查,类型检查分别会按照顺序对不同类型的节点进行验证,会按照如下顺序进行处理:
- 常量、类型、函数名及类型
- 变量的赋值和初始化
- 函数和闭包的主体
- 哈希键值对的类型
- 导入函数体
- 外部的声明
通过对每颗抽象结点树的遍历,会在每个节点上都会对当前子树的类型进行验证保证当前节点上不会出现类型错误的问题,所有的类型错误和不匹配都会在此阶段被发现和暴露出来。
类型检查不止会对树状结构的节点进行验证,同时也会对一些内建的函数进行展开和改写。比如make
关键字会在此阶段会根据子树的结构被替换成为makeslice
或makechan
等函数。
2. 变换
- 在抽象语法树上进行某些转换,某些节点基于类型信息会被细化。比如:从算术加法节点类型分割的字符串添加、死代码消除、函数调用内联、转义分析...
第三阶段:SSA生成(中间代码生成)
中间代码是指一种应用于抽象机器的编程语言,其设计目的是用来帮助我们分析计算机程序。在编译过程中,编译器会将源代码转换成目标机器上机器的过程中,先把原地阿玛转换成一种中间的表述形式。
当将源文件转换成抽象语法树、对整颗树的语法进行解析并进行类型检查之后,可以认为当前文件中的代码基本上不存在无法编译或语法错误的问题,Golang的编译器会将输入的AST转换成为中间代码。
- Golang编译器的中间代码具有静态单赋值(SSA)的特性
Golang编译器的中间代码使用了SSA(Static Single Assignment Form,静态单一分配)的特性,如果在中间代码生成的过程中使用此特性,就能够很容易的分析出代码中无用的变量和片段并对代码进行优化。
类型检查之后会通过名为compileFunctions
的函数开始对整个Golang中的全部函数进行编译,这些函数会在一个编译队列中等待几个后端工作goroutine
的消费,这些goroutine
会将所有函数对应的AST转换为使用SSA特性的中间代码。
抽象语法树(AST)将转换为静态单一分配(SSA)形式,SSA是一种具有特定属性的低级中间表示,可以更加轻松地实现优化并最终从中生成机器代码。
在此转换期间会将应用函数内在函数,对于这些特殊功能,编译器会教导它们根据具体情况使用大量优化的代码替代。
在AST到SSA转换期间,某些节点会被降级为更为简单的组件,因此编译器的其余部分可使用它们。例如内置复制替代为内存移动,并且范围循环被重写为for
循环。
然后,应用一系列与机器无关的传递和规则。这些不会涉及任何单个计算机体系结构,因此看在所有GOARCH变体上运行。
这些通用过程的示例包括消除死代码,删除不需要的零检查、删除未使用的分支。
通过重写规则主要涉及表达式,比如使用常量值替换某些表达式,以及优化乘法和浮点运算。
第四阶段:机器码生成
- 底层SSA和结构特定的传递
- 生成机器码
Golang源代码的cmd/compile/internal
中包含了许多机器代生成相关的包,不同类型的CPU分别使用不同的包进行生成amd64
、arm
、arm64
、mips
、mips64
、ppc64
、s390x
、x86
、wasm
。Golang能够在上述的CPU指令集类型上运行。
作为一种在栈虚拟机上使用的二进制指令格式,它的设计目标是在Web浏览器上提供一种具有高可移植性的目标语言。Golang编译器既能生成WASM格式的指令,就能运行在常见的主流浏览器中。
1$ GOARCH=wasm GOOS=js gobuild -o lib.wasm main.go
编译器的机器相关阶段以底层传递开始,该传递将通用值重写为其机器特定的变体。例如在AMD64存储器操作数上是可能的,因此可以组合许多加载存储操作。
需要注意的是,较低的通道运行所有特定于机器的重写规则,因此它当前也应用了大量优化。
一旦SSA降低且更加特定于目标体系结构,就会运行最终的代码优化过程。这包括另一个四代码消除传递,移动值更接近它们的使用,删除从未读取的局部变量,以及寄存器分配。
在SSA生成节点结束时,Go函数已转换为一些列obj.Prog
指令。它们会被传递给装载器cmd/internal/obj
,将它们转换为机器代码并写出最终的目标文件。目标文件还将包含反射数据,导出数据和调试信息。