### 本文源码版本为 GO 1.17.8 Windows/amd64;
### 可能参与对比的版本:GO 1.16.2 Linux/amd64
一、Golang的编译器究竟是如何工作的? (学习源码有什么意义)
1. 能修改go语言程序源码 (从某种意义上来说你可以定制一个属于你的语言)
2. 以语言开发者的角度去学习语言本身 (直接感受顶尖的设计思路,实用性算法和数据结构的学习)
3. 理解go语言语法糖以及原生性的实现原理 (拒绝八股式面试,直接认识实现逻辑)
二、修改go的源码
如何在调用fmt.Println()时,自动添加一条"hello"?
· 上一章中创建了一个hello.go文件,
并使用go build得到可执行文件,
./hello之后得到了hello,world的输出。
· 进入调试模式,找到调用的函数:
分屏1:gdb hello
分屏2:vim hello.go
可以发现在源码层面位于: (一层)
at /root/go/go/src/fmt/print.go:294
· 修改源码,重新编译编译器
修改为:
func Println(a ...interface{}) (n int, err error) {
println("hello")
return Fprintln(os.Stdout, a...)
}
重新编译编译器:
cd $GOROOT/src
.make.bash
· 检查执行效果
cd ../../repos
go build hello.go
./hello
显示结果除了 hello,world,在上面还有一行hello,成功!!!
三、基本概念
· 抽象语法树
1. 编程语言 通过 变量 类型 运算符 等基本要素去白标计算机要执行的操作
2. 输入的源码文件是一段字符串文本,结果词法分析后识别出哪些字符组成一个单词
3. 将的那次组织成语法树(多叉树),并将其中对编译过程无关的语法细节忽略,即组织成一颗抽象语法树
4. 抽象语法树就是用来描述语法结构的一种数据结构,是编译器能够识别的输入参数.
尝试一下看看go文件是怎么被解析的:
func main() {
src := `package main
import "fmt"
func main() {
v := make(map[string]string, 0)
v["hello"] = "world"
for k, v := range v {
fmt.Printf("k: %v\n", k)
fmt.Printf("v: %v\n", v)
}
}`
fileset := token.NewFileSet()
f, _ := parser.ParseFile(fileset, "", src, 0)
ast.Print(fileset, f)
}
输出的结构体为*ast.file:
type File struct {
Doc *CommentGroup // associated documentation; or nil
Package token.Pos // position of "package" keyword
Name *Ident // package name
Decls []Decl // top-level declarations; or nil
Scope *Scope // package scope (this file only)
Imports []*ImportSpec // imports in this file
Unresolved []*Ident // unresolved identifiers in this file
Comments []*CommentGroup // list of all comments in the source file
}
· 静态单赋值
1. 在生成的中间代码中,变量在整个生命周期内被赋值为一次,
那么就成此中间代码具有SSA特性。
2. 具有SSA特性的中间代码,会为编译器提供更多的优化空间
· 指令集
1. 汇编代码最终链接为可执行文件,在可执行文件中更多机器码是给处理器识别的操作命令;
2. 不同的处理器支持的操作命令集合是不同的,造成了跨平台移植性的问题。
四、Go编译器的主流程 (值得注意的是后面的gc指的是Go编译器,不是垃圾收集GC)
## src/cmd/compile/README.md
· 前端:
1. 词法和语法分析
a. 将源码字节流输入到词法解析器输出为token序列,去重空格逗号等无效字符;
b. 将token序列输入语法分析器按规定好的文法,转换为抽象语法树;
c. 在此过程中的语法错误会终止分析过程
2. 类型检查与转换AST
a. 遍历AST的节点进行类型检查
b. 改写关键字,展开语法糖
· 后端:
1. 转换为SSA,生成中间代码
将所有文件中的函数抽象出来加入一个队列,用协程并发生成为中间代码
2. 生成机器代码
链接不同的指令集生成对应平台的机器码
· 入口:
实际上gc.mian()才是真实实现的逻辑体现
https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/main.go
func main() {
// disable timestamps for reproducible output
log.SetFlags(0)
log.SetPrefix("compile: ")
buildcfg.Check()
archInit, ok := archInits[buildcfg.GOARCH]
if !ok {
fmt.Fprintf(os.Stderr, "compile: unknown architecture %q\n", buildcfg.GOARCH)
os.Exit(2)
}
gc.Main(archInit)
base.Exit(0)
}
五、词法和语法分析
· 词法分析
输入的字节流会在一个for循环中被迭代,
调用scanner对象的next方法获取下一个被识别的token序列。
拉取一次,下层才会执行一次,是一个拉模式的解析过程。
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
}
· 文法分析
所有的go文件都会被解析成一个AST,因此所有的语法顶层都是从一个SourceFile开始的;
最顶层的生产规则包含必选的包声明,以及可选的import声明和顶层声明;
顶层声明包括 常量、类型、别名、变量、函数 五大类,其中常量和变量声明可以使用语法课。
· 分析方法
1. 自顶向下
2. 自底向上
3. 向前查看
六、类型检查
1. 静态类型强调的是编译期就会对程序的类型系统进行全面检查,
而动态类型是指在编译期在将类型信息植入程序中使得在运行期间可以通过反射等方式操作对象实现动态功能。
2. 类型检查不仅检查类型的合法型,
同时也会完成对节点的类型转换为golang具体的类型系统的映射,并展开其中的语法糖
###https://github.com/golang/go/blob/f5978a09589badb927d3aa96998fc785524cae02/src/cmd/compile/internal/gc/typecheck.go#L1726
七、 中间代码生成
1. 初始化ssa配置
2. 函数替换:遍历并替换AST上某些关键字,将其转换为某个具体的运行时函数
3. 生成SSA中间代码:经过多轮的迭代,将AST中的节点进行不断的转换,将其中关键词替换成运行包中的具体内建函数的符号引用
查看生成的中间代码: go build -gcflags -S hello.go
查看编译器的优化过程: GOSSAFUNC=main go build main.go
八、机器码生成