Golang的命令go类似于一个工具箱,当我们执行go build时,go会进行包依赖分析,然后创建若干子进程去执行compile命令编译源码。go的入口函数是cmd/go/main.go:main。下面以go build为例简略分析这个过程。下文对代码做了非常多的精简,逻辑也做了简化。
一 main流程
main.go:main函数的的流程非常简单,如图所示。
go在解析完参数后,会查找对应的command。所有支持的command在main.go:init中初始化,并由internal目录下的各个包实现相对应的command。
func init() {
base.Go.Commands = []*base.Command{
bug.CmdBug,
work.CmdBuild,
list.CmdList,
run.CmdRun,
test.CmdTest,
}
}
如internal/work实现build命令,build.go中定义的CmdBuild如下所示。
var CmdBuild = &base.Command{
UsageLine: "go build [-o output] [-i] [build flags] [packages]",
Short: "compile packages and dependencies",
}
确定command后,main会调用command定义的Run函数,运行command。
二 运行command
func init() {
CmdBuild.Run = runBuild
CmdInstall.Run = runInstall
}
build.go初始化了CmdBuild的Run为runBuild函数。runBuild函数包含如下两个步骤
- 包依赖关系分析
- 根据依赖关系构造编译规则
- 调用compile编译源码
三 依赖分析
依赖分析主要是由internal/load和src/go/build两个包实现,涉及到三个函数:
- Import:分析go源文件,确定依赖关系。这一步涉及到语法解析。
- load:根据包依赖关系,递归导入所依赖的包。
- loadImport:先import,再load。
函数的调用关系如下图所示:
3.1 Import函数分析:
3.1.1 首先查找包。下图所示代码在GOPATH中查找包。
for _, root := range gopath {
dir := ctxt.joinPath(root, "src", path)
isDir := ctxt.isDir(dir)
binaryOnly = !isDir && mode&AllowBinary != 0 && pkga != "" && ctxt.isFile(ctxt.joinPath(root, pkga))
if isDir || binaryOnly {
p.Dir = dir
p.Root = root
goto Found
}
}
3.1.2 然后确定src/pkg/bin等目录
Found:
if p.Root != "" {
p.SrcRoot = ctxt.joinPath(p.Root, "src")
p.PkgRoot = ctxt.joinPath(p.Root, "pkg")
p.BinDir = ctxt.joinPath(p.Root, "bin")
}
3.1.3 最后解析源文件,确定依赖关系
for _, d := range dirs {
name := d.Name()
pf, err := parser.ParseFile(fset, filename, data, parser.ImportsOnly|parser.ParseComments)
for _, decl := range pf.Decls {
d, ok := decl.(*ast.GenDecl)
for _, dspec := range d.Specs {
spec, ok := dspec.(*ast.ImportSpec)
quoted := spec.Path.Value
path, err := strconv.Unquote(quoted)
if else {
imported[path] = append(imported[path], fset.Position(spec.Pos()))
}
}
}
if else {
p.GoFiles = append(p.GoFiles, name)
}
}
p.Imports, p.ImportPos = cleanImports(imported)
3.2 load函数分析
3.2.1 首先填充package信息,包括名称,路径等
p.copyBuild(bp)
3.2.2 然后递归地进行依赖分析
importPaths包括依赖包的导入路径,p.Internal.Imports包括依赖包的Package对象。LoadImport函数内部会调用loadImport函数。
imports := make([]*Package, 0, len(p.Imports))
for i, path := range importPaths {
p1 := LoadImport(path, p.Dir, p, stk, p.Internal.Build.ImportPos[path], ResolveImport)
path = p1.ImportPath
importPaths[i] = path
}
p.Internal.Imports = imports
总结下:经过以上的分析后,最终可以得到如下所示的依赖关系
四 根据依赖关系构建编程规则
main包由LinkAction函数处理。下面的代码片段显示会根据main包的依赖关系再调用CompileAction函数构建编译规则。
a := b.cacheAction("link", p, func() *Action {
a := &Action{
Mode: "link",
Package: p,
}
a1 := b.CompileAction(ModeBuild, depMode, p)
a.Func = (*Builder).link
a.Deps = []*Action{a1}
a.Objdir = a1.Objdir
a1.Deps = append(a1.Deps, &Action{Mode: "nop", Deps: a.Deps[1:]})
return a
})
下图的CompileAction片段显示根据p.Internal.Imports中存在的依赖关系对象进行递归构建。
a := b.cacheAction("build", p, func() *Action {
a := &Action{
Mode: "build",
Package: p,
Func: (*Builder).build,
}
if p.Error == nil || !p.Error.IsImportCycle {
for _, p1 := range p.Internal.Imports {
a.Deps = append(a.Deps, b.CompileAction(depMode, depMode, p1))
}
}
return a
})
五 调用compile编译源码
work包根据指定的编译规则进行编译
5.1 确定源代码,并调用gc函数
下面的代码片段显示从package对象中提出go, cxx等源文件名
gofiles := str.StringList(a.Package.GoFiles)
cgofiles := str.StringList(a.Package.CgoFiles)
cfiles := str.StringList(a.Package.CFiles)
sfiles := str.StringList(a.Package.SFiles)
cxxfiles := str.StringList(a.Package.CXXFiles)
ofile, out, err := BuildToolchain.gc(b, a, objpkg, icfg.Bytes(), symabis, len(sfiles) > 0, gofiles)
5.2 gc函数会准备好相应的参数,并调用runOut函数
args := []interface{}{cfg.BuildToolexec, base.Tool("compile"), "-o", ofile,
"-trimpath", trimDir(a.Objdir), gcflags, gcargs, "-D", p.Internal.LocalPrefix}
output, err = b.runOut(p.Dir, nil, args...)
5.3 runOut函数依赖exec包创建子进程,执行compile命令
cmdline := str.StringList(cmdargs...)
cmd := exec.Command(cmdline[0], cmdline[1:]...)
err := cmd.Run()
那么最后子进行执行compile命令类似于下面这样的代码:
/path/golang/1.12.3/go/pkg/tool/linux_amd64/compile \
-o /tmp/go-build205483930/b001/_pkg_.a \
-trimpath /tmp/go-build205483930/b001 \
-p main
-complete \
-buildid 2Kk1bX-iMGXElVG-VfaD/2Kk1bX-iMGXElVG-VfaD \
-goversion go1.12.3 \
-D
-importcfg /tmp/go-build205483930/b001/importcfg
-pack \
-c=4 \
/path/work/project/src/xx.com/main.go
link命令类似于下面这样的代码:
/path/golang/1.12.3/go/pkg/tool/linux_amd64/link \
-o /tmp/go-build205483930/b001/exe/a.out \
-importcfg /tmp/go-build205483930/b001/importcfg.link \
-buildmode=exe -buildid=q2TSg1YBVSgfvTEOCX3Z/2Kk1bX-iMGXElVG-VfaD/JK1t1fZfHphfRSKuZ2gn/q2TSg1YBVSgfvTEOCX3Z \
-extld=gcc /tmp/go-build205483930/b001/_pkg_.a