写在文章开头
写了这么久的Go语言,慢慢也有了一些读者的关注,但是大部分读者都还是Java(笑)
,而笔者今天准备分享的,则是关于Go语言的编译过程。
Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。
Go语言编译过程详解
词法分析
假设我们此时用goland
写下面这样基础代码:
package main
import "fmt"
func main() {
fmt.Println("hello Go")
}
编译时首先会经过词法分析,词法分析主要做的就是将代码中的最小语义生成token
,而笔者这里所说的最小语义,读者完全可以理解为上述的每一个关键字,例如package
、import
等。
句法分析
完成词法分析之后就是句法分析了,它会基于上述的token
序列生成语法树,大体如下这段笔者的示例图所示:
语义分析
完成了词句的分析之后,就是语义分析了,通过语义了解代码的作用,这一步会涉及代码的各种检查和优化,例如:
- 类型检查:因为
go
也是和Java
、c#
一样是一门强类型语言,所以编译时会对类型进行检查,再编译时检查当代码中的类型是否安全。 - 类型推断:go语言通过字面量初始化是无需声明类型的,其语法如
i:=2
,所以在语义分析阶段,go语言也会针对这些代码段进行语义分析。 - 类型是否匹配
- 函数内联:对于某些函数,Go语言会在编译期对当函数的调用出进行内联优化,从而避免函数调用的堆栈调用的开销,可能笔者这里说的有点拗口,举个例子,假如我们写了一个add函数其功能和调用代码如下:
func main() {
sum := add(1, 2)
fmt.Println(sum)
}
func add(num1 int, num2 int) int {
return num1 + num2
}
go在进行语义分析时,通过函数内联,可能会将其优化成下面这样:
func main() {
sum := 1+2
fmt.Println(sum)
}
- 逃逸分析:关于逃逸分析笔者会在后续的文章中展开说明,这里简单了解一下逃逸分析则是判断当前函数内的对象是否被外部引用,由此推断其是否发生逃逸,从而决定当前这个对象示例是分配在堆上还是栈上。
生成中间码
在生成各个系统平台可执行的机器码之前,go会生成一段与平台无关的中间汇编码,即可SSA码,在此期间,代码可能还会再进行一次优化工作。
对于SSA码,感兴趣的读者可以在操作系统上通过这段指令生成:
GOSSAFUNC=main go build main.go
执行完成之后,文件夹会生成一段ssa.html
,读者打开之后就会看到下面这样一个网页,其中网页的最右边就是我们说的SSA
码,由于SSA
码不是笔者本次讨论的重点就是就不做展开了:
生成汇编码
通过上述的步骤之后,系统就会得到中间码,自此各个平台都会基于这段中间码生成汇编码,当然如果你对汇编码感兴趣,可以通过下面这段执行看到我们的代码转为Plan 9
的汇编码:
go build -gcflags -S main.go
可以看到一行简单的输出语句就变成下面这样一段汇编代码:
链接
基于上述的代码键入如下指令即可查看go语言的编译过程:
go build -n main.go
此时在Linux
终端就会输出一大段日志,这里笔者就贴出几个比较核心的地方,首先就是导入配置,由上代码我们可知我们用到了go
语言最基本的runtime
和fmt
包:
# import config
packagefile fmt=/root/.cache/go-build/7a/7a84f8c71e0cd98a53158ab655d48960d612698abe0567abbeb7a633bcb066b7-d
packagefile runtime=/root/.cache/go-build/e2/e2bf522ce6c0c2bfb09b8486578b70b1424422349a8dc2c5e200bf6b8760d950-d
EOF
随后就开始通过compile
完成上述所说的编译过程:
cd /root
/usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -complete -buildid 5LGDePcnhcnEtpXVckY4/5LGDePcnhcnEtpXVckY4 -goversion go1.22.0 -c=2 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./main.go
/usr/local/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cat >$WORK/b001/importcfg.link << 'EOF' # internal
.....
中间完成中间码和汇编码生成机器码之后,就来到了链接这一步,如下输出所示,可以看到它用到了/usr/local/go/pkg/tool/linux_amd64/link
cd .
/usr/local/go/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=IGC7T6g3raqmSVvDtHEN/5LGDePcnhcnEtpXVckY4/5LGDePcnhcnEtpXVckY4/IGC7T6g3raqmSVvDtHEN -extld=gcc $WORK/b001/_pkg_.a
/usr/local/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal
最终在最后一段输出我们得到了可执行文件main
,自此我们的go代码编译过程完成:
mv $WORK/b001/exe/a.out main
小结
我们再简单的小结一下这篇文章的内容,本文给出了一段比较简单的go语言示例代码,通过go工具包所提供的各种指令解释并查看了以下几个步骤的详细工作过程,关于Go语言的编译过程,其整体步骤为:
- 词法分析
- 句法分析
- 语义分析
- 生成中间码
- 生成机器码
- 链接构成可执行文件
我是 sharkchili ,CSDN Java 领域博客专家,开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。
参考
Go 语言设计与实现 :https://draveness.me/golang/