1.1 调试源代码 #
各位读者朋友,很高兴大家通过本博客学习 Go 语言,感谢一路相伴!《Go语言设计与实现》的纸质版图书已经上架京东,有需要的朋友请点击 链接 购买。
Go 语言作为开源项目,我们可以很轻松地获取它的源代码,它有着非常复杂的项目结构和庞大的代码库,今天的 Go 语言中差不多有 150 万行源代码,其中包含将近 140 万行的 Go 语言代码,我们可以使用如下所示的命令查看项目中代码的行数:
$ cloc src
5988 text files.
5875 unique files.
1165 files ignored.
github.com/AlDanial/cloc v 1.78 T=6.96 s (693.7 files/s, 274805.2 lines/s)
-----------------------------------------------------------------------------------
Language files blank comment code
-----------------------------------------------------------------------------------
Go 4199 139910 221375 1398357
Assembly 486 12784 19137 106699
C 64 718 562 4587
JSON 12 0 0 1712
...
-----------------------------------------------------------------------------------
SUM: 4828 154344 242395 1515787
-----------------------------------------------------------------------------------
随着 Go 语言的不断演进,整个代码库也会随着时间不断变化,所以上面的统计结果每天都有所不同。虽然该项目有着巨大的代码库,但是想要调试 Go 语言并不是不可能的,只要我们掌握合适的方法并且对 Go 语言的标准库有一些了解,就可以调试 Go 语言,我们在这里会介绍一些编译和调试 Go 语言的方法。
1.1.1 编译源码 #
假设我们想要修改 Go 语言中常用方法 fmt.Println
的实现,实现如下所示的功能:在打印字符串之前先打印任意其它字符串。我们可以将该方法的实现修改成如下所示的代码片段,其中 println
是 Go 语言运行时提供的内置方法,它不需要依赖任何包就可以向标准输出打印字符串:
func Println(a ...interface{}) (n int, err error) {
println("draven")
return Fprintln(os.Stdout, a...)
}
当我们修改了 Go 语言的源代码项目,可以使用仓库中提供的脚本来编译生成 Go 语言的二进制以及相关的工具链:
$ ./src/make.bash
Building Go cmd/dist using /usr/local/Cellar/go/1.14.2_1/libexec. (go1.14.2 darwin/amd64)
Building Go toolchain1 using /usr/local/Cellar/go/1.14.2_1/libexec.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 using go_bootstrap and Go toolchain2.
Building packages and commands for darwin/amd64.
---
Installed Go for darwin/amd64 in /Users/draveness/go/src/github.com/golang/go
Installed commands in /Users/draveness/go/src/github.com/golang/go/bin
./src/make.bash
脚本会编译 Go 语言的二进制、工具链以及标准库和命令并将源代码和编译好的二进制文件移动到对应的位置上。如上述代码所示,编译好的二进制会存储在 $GOPATH/src/github.com/golang/go/bin
目录中,这里需要使用绝对路径来访问并使用它:
$ cat main.go
package main
import "fmt"
func main() {
fmt.Println("Hello World")
}
$ $GOPATH/src/github.com/golang/go/bin/go run main.go
draven
Hello World
我们会发现上述命令成功地调用了我们修改后的 fmt.Println
函数,而在这时如果直接使用 go run main.go
,很可能会使用包管理器安装的 go
二进制,得不到期望的结果。
1.1.2 中间代码 #
Go 语言的应用程序在运行之前需要先编译成二进制,在编译的过程中会经过中间代码生成阶段,Go 语言编译器的中间代码具有静态单赋值(Static Single Assignment、SSA)的特性,我们会在后面介绍该中间代码的该特性,在这里我们只需要知道这是一种中间代码的表示方式。
很多 Go 语言的开发者都知道我们可以使用下面的命令将 Go 语言的源代码编译成汇编语言,然后通过汇编语言分析程序具体的执行过程:
$ go build -gcflags -S main.go
rel 22+4 t=8 os.(*file).close+0
"".main STEXT size=137 args=0x0 locals=0x58
0x0000 00000 (main.go:5) TEXT "".main(SB), ABIInternal, $88-0
0x0000 00000 (main.go:5) MOVQ (TLS), CX
0x0009 00009 (main.go:5) CMPQ SP, 16(CX)
...
rel 5+4 t=17 TLS+0
rel 40+4 t=16 type.string+0
rel 52+4 t=16 ""..stmp_0+0
rel 64+4 t=16 os.Stdout+0
rel 71+4 t=16 go.itab.*os.File,io.Writer+0
rel 113+4 t=8 fmt.Fprintln+0
rel 128+4 t=8 runtime.morestack_noctxt+0
然而上述的汇编代码只是 Go 语言编译的结果,作为使用 Go 语言的开发者,我们已经能够通过上述结果分析程序的性能瓶颈,但是如果想要了解 Go 语言更详细的编译过程,我们可以通过下面的命令获取汇编指令的优化过程:
$ GOSSAFUNC=main go build main.go
# runtime
dumped SSA to /usr/local/Cellar/go/1.14.2_1/libexec/src/runtime/ssa.html
# command-line-arguments
dumped SSA to ./ssa.html
上述命令会在当前文件夹下生成一个 ssa.html
文件,我们打开这个文件后就能看到汇编代码优化的每一个步骤:
图 1 - 1 SSA 示例
上述 HTML 文件是可以交互的,当我们点击网页上的汇编指令时,页面会使用相同的颜色在 SSA 中间代码生成的不同阶段标识出相关的代码行,更方便开发者分析编译优化的过程。
1.1.3 小结 #
掌握调试和自定义 Go 语言二进制的方法可以帮助我们快速验证对 Go 语言内部实现的猜想,通过最简单粗暴的 println
函数可以调试 Go 语言的源码和标准库;而如果我们想要研究源代码的详细编译优化过程,可以使用上面提到的 SSA 中间代码深入研究 Go 语言的中间代码以及编译优化的方式,不过只要我们想了解 Go 语言的实现原理,阅读源代码是绕不开的过程。