在 # Go语言编译器的正确打开方式(一)- 从源码编译 go 中,我们做好了debug go源码的前期准备。
学会debug跟踪Go编译的源码,对于理解Go编译过程至关重要。
通过在关键位置暂停,我们可以更深入地了解编译的不同阶段,以及AST和IR的结构
观察 go build 过程
使用go build -x
命令,可以详细查看Go编译的全过程,包括 编译(compile) 和 链接(link) 两大步骤。
以gorm代码为例,跳转码utils
目录下,并执行go build -x
命令。
可以看到编译utils.go
文件的具体步骤,如创建目录、写入importcfg文件等。

完整输出如下
bash
WORK=/var/folders/nv/f11l1jld7ql771rfz_b21yhc0000gr/T/go-build3026780938
mkdir -p $WORK/b001/
cat >/var/folders/nv/f11l1jld7ql771rfz_b21yhc0000gr/T/go-build3026780938/b001/importcfg << 'EOF' # internal
# import config
packagefile database/sql/driver=/Users/hangwu/Library/Caches/go-build/2e/2e6a3a4ec553e101f2e36fd8b0049a72f9ba9e349a0f5a3c22b3b8b3ad090229-d
packagefile fmt=/Users/hangwu/Library/Caches/go-build/04/04764c52faa1d3e6e698518e1dc3b3ffae4c25ac7875fe814b3fbf9ca8963b8c-d
packagefile path/filepath=/Users/hangwu/Library/Caches/go-build/8f/8fa7f7e56ec93875e3135dd3e55a91129cfee1a57957e0ca012b09d8f12822a2-d
packagefile reflect=/Users/hangwu/Library/Caches/go-build/f7/f79d69d68777453429cb7edbe0d5be687a91314798b7943367822cefba2aff48-d
packagefile runtime=/Users/hangwu/Library/Caches/go-build/a5/a5f3be7ffcd435cd4c0f4a05cbc6191cd7f9b3820831da31d77836525ca793d0-d
packagefile strconv=/Users/hangwu/Library/Caches/go-build/5f/5f04c11b590cb2a403b65d7cc2338fb5f42b711c927b98a3b423ba42a8b8d742-d
packagefile strings=/Users/hangwu/Library/Caches/go-build/db/db137e54b24d29ba4dc77debbdee4b4a69497480b580d888ea6b2cc072fccddd-d
packagefile unicode=/Users/hangwu/Library/Caches/go-build/92/92c0c5fa656d96d5befb9e4fffc51083c1a5170e54b6876a8993ce9415d24de8-d
EOF
cd /Users/hangwu/0_studio/github.com/gorm/utils
/Users/hangwu/0_studio/github.com/golang/go/pkg/tool/darwin_arm64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p gorm.io/gorm/utils -lang=go1.18 -complete -buildid UOPFRWgEHothTi9USrEK/UOPFRWgEHothTi9USrEK -goversion go1.21.11 -c=4 -shared -nolocalimports -importcfg $WORK/b001/importcfg -pack ./utils.go
/Users/hangwu/0_studio/github.com/golang/go/pkg/tool/darwin_arm64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /Users/hangwu/Library/Caches/go-build/0b/0bd61e4e037c153dc08d70e9cdf18fcd2b5e194695984ae0f2875fe6bdbc03f5-d # internal
启动 Debug
从上面的输出,我们注意到 compile
命令正是go编译的过程,我们需要模仿 compile
的命令,然后启动debug。
第一步,构造importcfg文件
在utils
目录下创建importcfg
文件,将上面# import cfg
到EOF
中间的内容拷贝到文件中。

编译过程中,依赖的模块会先被编译,并以靜态库的方式,缓存在Caches
目录下,当前代码的编译会查找importcfg
中指定的文件并引用。
第二步,配置GOROOT(以Goland为例)
将 # Go语言编译器的正确打开方式(一)- 从源码编译 go 中的go源码目录配置为GOROOT

第三步,依样画葫芦,配好go compile的运行参数
这里只需要摘抄第一步中go build -x
命令过程中编译参数
bash
/Users/hangwu/0_studio/github.com/golang/go/pkg/tool/darwin_arm64/compile -o
$WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p gorm.io/gorm/utils -lang=go1.18 -complete -buildid UOPFRWgEHothTi9USrEK/UOPFRWgEHothTi9USrEK -goversion go1.21.11 -c=4 -shared -nolocalimports -importcfg $WORK/b001/importcfg -pack ./utils.go
如果想知道这些参数控制什么,请用命令
go tool compile --help
查看帮助文档

点击Run
运行,配置正确的话,在gorm/utils
目录下生成一个编译后的_pkg_.a
文件

第四步,debug一个示例
通过阅读go源码中src/cmd/compile
目录下的README.md
文件,了解go编译器的结构。
要注意不同go版本的这个文件内容是不一样的,注意尽量阅读当前版本的文件。
通过这个文档,我们知道了cmd/compile/internal/syntax
目录下是编译的前端,也是编译的第一阶段,包括词法分析,语法分析,构造 AST(抽像语法树)
我们找到noder.go
的LoadPackage
函数, 在最后的unified
调用处打上断点。 unify
之前是 AST ,unifiy
之后是 IR

IR 是 Internal Representation 的缩写,将代码转换为底层操作指令的中间产物 每个编译器都会定义自己的IR,后续的阶段都是基于IR进行优化并生成最终底层代码
一个AST的例子
接下来将第三步的配置以Debug方式启动,程序会停到断点这里。
观察这里的内存变量。要知道go编译器是把每个文件都扫描生成一个AST,一对一非常直观。

看到noders
数组,每个noder代表一个文件的AST,当前包下只有utils.go
一个文件,所以数组长度为1。
具体函数的例子
再展开,能看到代码被扫描成不同的syntax.Decl
,包括了前面是一些import语句,var全局变量定义语句,以及后面是一些函数定义。

这里的Decl
还是按代码顺序整理的,所以可以比较方便的看到我们想要的。
打开最后一个Decl
结构,可以看到对应代码文件中的RTrimSlice
函数。它包含了两个if
语句(Stmt指Statment),和一个return
语句。

后续
如果上面的过程顺利,那么你就具备了跟踪编译所有阶段的能力,
通过在不同的地方打断点跟踪,甚至修改代码,可以更好地理解Go编译过程。