前言:
让我们一起来了解下go build命令都做了些啥;并进行源码追踪其过程;在golang早期版中编译器,连接器都是用C开发的。后期版本中go的编译器连接器都用go重写了一套,这一套都是开源的,我们都可以阅读;
golang版本: go1.13.4 darwin/amd64
调试工具: dlv
dlv如果不太会用的可以看一下我前一篇文章:好未来技术交流社区(TTC)
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文件;
如图
结束:
分析源码是一个比较枯燥的过程,需要反复尝试。不过这个过程能让你学习到一些设计理解,对于成长是有帮助的,大家一起共勉;