前言:
让我们一起来了解下go build命令都做了些啥;并进行源码追踪其过程;在golang早期版中编译器,连接器都是用C开发的。后期版本中go的编译器连接器都用go重写了一套,这一套都是开源的,我们都可以阅读;
golang版本: go1.13.4 darwin/amd64
调试工具: dlv
dlv如果不太会用的可以看一下我前一篇文章:https://ttc.zhiyinlou.com/#/articleDetail?id=486
go build命令参数可选项:
可选项 | 备注 |
-n | 打印编译过程 |
-a | 将命令源码文件与库源码文件全部重新构建 |
-x | 打印编译期间用到的命名,它与 -n 的区别是,它不仅打印还会执行 |
-o | 输出执行文件保存的文件名 |
-race | 开启竞态条件的检测,支持的平台有限制 |
一.过程解析:
输入如下命令,可以看到图中:
#go build -n default.go
上图中过程,和上图中略有不同的地方是cat >$WORK/b001/_gomod_.go 这行没有写入;这一块是由于gomod的缘故;这个不是关键点所以没有写入;
#创建目录 mkdir -p $WORK/b001/ #编译文件 cd /data/webroot/qingke/godemo/dump /usr/local/Cellar/go/1.13.4/libexec/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -complete -buildid Rw-tIgJPD3wmhn5zj7iu/Rw-tIgJPD3wmhn5zj7iu -goversion go1.13.4 -D _/data/webroot/qingke/godemo/dump -importcfg $WORK/b001/importcfg -pack -c=4 ./default.go $WORK/b001/_gomod_.go #生成链接库配置importcfg.link文件 cat >$WORK/b001/importcfg.link << 'EOF' # internal packagefile command-line-arguments=$WORK/b001/_pkg_.a packagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.a packagefile internal/bytealg=/usr/local/go/pkg/darwin_amd64/internal/bytealg.a packagefile internal/cpu=/usr/local/go/pkg/darwin_amd64/internal/cpu.a packagefile runtime/internal/atomic=/usr/local/go/pkg/darwin_amd64/runtime/internal/atomic.a packagefile runtime/internal/math=/usr/local/go/pkg/darwin_amd64/runtime/internal/math.a packagefile runtime/internal/sys=/usr/local/go/pkg/darwin_amd64/runtime/internal/sys.a EOF #创建b001中exe目录 mkdir -p $WORK/b001/exe/ cd . #连接生成a.out可执行文件 /usr/local/Cellar/go/1.13.4/libexec/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=TwK52M06SyTj6MiQ9MRi/Rw-tIgJPD3wmhn5zj7iu/Rw-tIgJPD3wmhn5zj7iu/TwK52M06SyTj6MiQ9MRi -extld=clang $WORK/b001/_pkg_.a #更新a.out id /usr/local/Cellar/go/1.13.4/libexec/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal #mv a.out改变名为default的可执行程序 mv $WORK/b001/exe/a.out default
上面部分有三个命令link、buildid、compile这三个命令是编译的核心;
compile: 编译器
link: 连接器
buildid: ID生成器
二.源码分析:
执行调试go命令,dlv需要用完整路径,可以用which go查看一下
命令执行如下:
dlv exec /usr/local/Cellar/go/1.13.4/libexec/bin/go build default.go
设置字符串打印长度,为了查看变量,不设置的话,变量看不完整
(dlv)config max-string-len 99999
然后下一个断点:
(dlv)b main.main
如图所示:
我打印了args数组的结果,其实就是我们go后面的build 和 default.go参数;
在此时我们可要注意src/cmd/go/main.go代码中有base.Go.Commands的初始化,可以简单看一下Command结构
结构原型在src/cmd/go/internal/base/base.go代码中:
type Command struct { //命令运行方法 Run func(cmd *Command, args []string) //命令行提示信息 UsageLine string //go help中简短描述 Short string //go help中详细描述 Long string //命令标志 Flag flag.FlagSet //CustomFlags指示命令将执行其自己的标志解析。 CustomFlags bool // Commands lists the available commands and help topics. // The order here is the order in which they are printed by 'go help'. // Note that subcommands are in general best avoided. Commands []*Command }
此时可以可以注意到base.Go.Commands中的work.CmdBuild对应的就是我们build命令的映射。
在我们的src/cmd/go/internal/work/build.go中;work.CmdBuild的初始化在build.go中进行初始化,对应方法runBuild。
此时可以对runBuild下一个断点继续跟踪;
打印387行中的a
结构体在src/cmd/go/internal/work/action.go中,结构如下:
type Action struct { Mode string // 动作操作说明 Package *load.Package // 此操作的工作包 Deps []*Action // 在此之前必须采取的行动 Func func(*Builder, *Action) error // 动作方法(nil = no-op) IgnoreFail bool // 即使依赖项失败,是否运行f TestOutput *bytes.Buffer // 测试输出缓冲区 Args []string // 运行程序的其他参数 triggers []*Action // inverse of deps buggyInstall bool // is this a buggy install (see -linkshared)? TryCache func(*Builder, *Action) bool // callback for cache bypass // Generated files, directories. Objdir string // 中间对象目录 Target string // 操作的目标:创建的包或可执行文件 built string // 实际创建的包或可执行文件 actionID cache.ActionID // 动作输入的缓存ID buildID string // 操作输出的生成ID VetxOnly bool // Mode=="vet": only being called to supply info about dependencies needVet bool // Mode=="build": need to fill in vet config needBuild bool // Mode=="build": need to do actual build (can be false if needVet is true) vetCfg *vetConfig // vet config output []byte // output redirect buffer (nil means use b.Print) // Execution state. pending int // number of deps yet to complete priority int // relative execution priority Failed bool // whether the action failed json *actionJSON // action graph information }
该结构体主要存储动作行为;比如说执行编译动作,然后通过结构体映射到对应方法;
然后我们看一下Do方法,Do方法其实就是对动作的操作;
func (b *Builder) Do(root *Action) { ... // Write action graph, without timing information, in case we fail and exit early. writeActionGraph := func() { if file := cfg.DebugActiongraph; file != "" { if strings.HasSuffix(file, ".go") { // Do not overwrite Go source code in: // go build -debug-actiongraph x.go base.Fatalf("go: refusing to write action graph to %v\n", file) } js := actionGraphJSON(root) if err := ioutil.WriteFile(file, []byte(js), 0666); err != nil { fmt.Fprintf(os.Stderr, "go: writing action graph: %v\n", err) base.SetExitStatus(1) } } } writeActionGraph() b.readySema = make(chan bool, len(all)) ... //Handle运行单个操作并负责触发因此可运行的任何操作。 handle := func(a *Action) { if a.json != nil { a.json.TimeStart = time.Now() } var err error if a.Func != nil && (!a.Failed || a.IgnoreFail) { err = a.Func(b, a) //执行事件动作 } ... } ... // Write action graph again, this time with timing information. writeActionGraph() }
请注意之前打印387行中的a的
动作其实会执行build方法,其实我们可以针对build方法下一个断点;执行
(dlv) b cmd/go/internal/work.(*Builder).build (dlv) c (dlv) bt
跟踪到397行进入buildActionID方法
按n继续执行
这一块的compile是根据base.Tool获取,不同的操作系统获取的编译器不一致.继续按n执行
exec.Comand方法其实就是调用complie去编译default.go文件;
complie命令会生成一个/var/folders/24/m8_dx27s2g3dqxypxcll0kvw0000gn/T/go-build086150708/b001/exe/a.out
然而/var/folders/24/m8_dx27s2g3dqxypxcll0kvw0000gn/T/go-build086150708/就是我们最初看到的$work,这个每次是不一样的。
然而想得到 complie,link,buildid命令操作都可以对 cmd/go/internal/base.Tool下断点去跟踪,看到对应的过程;
其实在调用complie命令之后,又调用了asm命令。
asm命令其实是生成.o文件;
除此之外,并不是每次go build时都会去调用complie命令,有时候则不会。在程序id一致的时候就会去取对应编译的cache文件;
如图
结束:
分析源码是一个比较枯燥的过程,需要反复尝试。不过这个过程能让你学习到一些设计理解,对于成长是有帮助的,大家一起共勉;