Golang的命令go类似于一个工具箱,当我们执行go build时,go会进行包依赖分析,然后创建若干子进程去执行compile命令编译源码。go的入口函数是cmd/go/main.go:main。下面以go build为例简略分析这个过程。下文对代码做了非常多的精简,逻辑也做了简化。

一 main流程

main.go: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

总结下:经过以上的分析后,最终可以得到如下所示的依赖关系

package依赖关系示例

四 根据依赖关系构建编程规则

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