go build命令用来构建一个go package,最常用的就是构建一个package得到一个可执行文件。build命令接受很多参数用来控制构建过程。本文使用的Golang源码的版本是1.7.5,这个版本的Go是自举的,也就是说Go构建器本身也是Go写的。

代码路径约定: Golang的仓库中所有的代码在子目录src/下,所以下面提到代码路径时,会把前缀src/去掉,避免表述过于冗长。

package中的符号: go/build.Context表示go/build这个package中的Context这个表达式。

另外,因为go build命令的参数很多,支持的操作系统和架构平台也很多,这里只描述最简单的参数的情况下的代码路径。操作系统是Linux,架构平台是x86_64,在Golang源码里称为amd64

go build命令入口

Golang的源码中,go build命令的入口在cmd/go/build.gorunBuild(cmd *Command, args []string)函数。该函数会在cmd/go/main.gomain()函数中被调用。调用前,会先解析所有的命令行参数,解析的参数会设置到cmd/go/build.go文件的全局变量中,比如buildAbuildN等。其他的非参数部分,就会通过runBuild()函数的args参数传递进来。

  • 当你执行go build -a a.go b.go时,args的内容会是{"a.go", "b.go"}
  • 当你执行go build -a goexample/hello goexample/foo时,args的内容会是{"goexample/hello", "goexample/foo"}

runBuild函数进来后,会先执行instrumentInit()buildModeInit()这两个函数。instrumentInit()函数处理-race-msan两个参数,进行一些初始化配置。buildModeInit()函数根据-buildmode参数的值来设置一些全局变量。-buildmode的默认值是"default"。上面这两个函数我们先不用关心,不影响主线代码的阅读。

接下来,runBuild()函数会调用packagesForBuild(args)获得需要构建的pkg的信息。

packagesForBuild函数

packagesForBuild(args []string) []*Package函数位于cmd/go/pkg.go文件中。它的功能是载入参数args指定的packages以及这些packages的依赖,生成一个Package对象列表,这个列表随后会被用于构建。

packagesForBuild()本身并不执行载入packages的工作,而是调用packagesAndErrors()函数来载入packages,然后检查载入的packages是否有错误,有错的话就让go build命令报错并返回。

packagesAndErrors函数

packagesAndErrors(args []string) []*Package函数也是位于cmd/go/pkg.go文件中,它开始执行载入packages的操作。分为两种情况:

  1. 如果args中都是文件名,比如go build -a a.go b.go,那么会调用goFilesPackage(args)来执行载入package的操作。
  2. 否则,就是go build -a goexample/hello goexample/foo这种情况。

第一种情况我们先跳过,回头再来看。先来看第二种情况,也就是更常见的情况。我们先限制命令行参数只传递了一个package goexample/hello,然后来看下代码:

  1. args = importPaths(args),该函数处理命令行传递进来的packages的特殊情况,比如Windows系统、路径中多余的部分、std/cmd/all这些保留名称、扩展符等,然后返回所有所有需要处理packages。此时返回的每个package都可以认为是一个ImportPath。因为我们举的例子goexample/hello*都不在特殊情况里面,所以它会原样返回。
  2. 针对args中的每个ImportPath,调用loadPackage(arg, &stk)来载入这个package。其中stk参数是一个表示依赖深度的栈,后面再仔细说明。在这一步,就得到了所需的Package对象。
  3. 最后,调用computeStale()函数,该函数会判断一个package是否需要重新构建。如果go build传递了-a参数,那么所有的packages都要重新构建。

从上面的流程可以看出,载入一个package的主要工作都是在loadPackage()函数中完成的。

loadPackage函数

loadPackage(arg string, stk *importStack) *Package位于cmd/go/pkg.go文件中,它载入一个命令行参数指定的package。stk参数是用于记录import路径的一个栈。

该函数的流程分为4个部分:

  1. 判断arg是否是local import。Local Import表是一个ImportPath是”.”、”..”、”./”开头后者是”../”开头。是的话,就对arg进行特殊处理。
  2. 判断arg是否以cmd/开头且cmd/之后没有/。是的话表示构建的是一个go命令。会在这个分支里执行载入,然后返回。
  3. 如果构建的不是一个go命令,那么就是一个package。会先处理一下local import一个standard package的情况,也就是说使用local import的方式指定一个standard package的情况。standard package是指GOROOT中的那些package
  4. 最后,调用loadImport(arg, cwd, nil, stk, nil, 0)来进行arg的载入。

对于我们的简单例子,在这里会直接走到最后的loadImport调用。

importStack

这个是一个简单的栈实现,用来在载入一个package的过程中,记录import的路径,栈底是命令行指定要载入的package,栈顶是当前正在处理的ImportPath。

loadImport函数

loadImport(path, srcDir string, parent *Package, stk *importStack, importPos []token.Position, mode int) *Package位于cmd/go/pkg.go文件中。几个参数的含义如下:

  • path,package的ImportPath,可能是local import的。
  • srcDir,是执行命令时的当前路径,用于当arg是local import path的情况下,查找package用的。
  • parent,是path的父package,也就是把path import进来的package。
  • stk,记录package import路径的栈。
  • importPos,记录import语句的位置用的,以便出错时可以显示出位置信息。
  • mode,目前只有一个mode,useVendor,表示是否需要进行vendor展开。useVendor为真,表示一个文件中的import path还没有被转换为一个vendor中的path,需要被进行vendor展开处理。当我们从loadPackage中调用loadImport时,传递的mode为0,因为我们要构建的package显然不会在vendor目录中。

下面来看loadImport中的主要流程。这个函数中还考虑了vendorinternal的情况,我们先跳过这些,只看主要步骤:

  1. stk.push(path)path进行入栈处理,这个栈的作用上面已经讲过。
  2. 通过packageCache全局变量,判断这个path是否已经被处理过了,如果已经处理过,就直接进行重用,调用reusePackage(p, stk)
  3. 生产一个新的Package对象p,并且加入packageCache
  4. 调用bp, err := buildContext.Import(path, srcDir, buildMode)来获得这个path对应的go/build.Package对象。
  5. 调用p.load(stk, bp, err)把这个build.Package对象载入到Package对象p中。
  6. 返回p

上面这个流程里有几个新的东西,我们先来看一下。

  1. reusePackage函数,这个函数的主要作用是判断是否存在循环应用的情况。在后面的文章会讲解判断方法。

  2. buildContext

    cmd/go/build.go文件中的buildContext变量是用来保存构建上下文的全局变量,默认指向go/build.Default

    注: go/build这个package是用来收集一个go package有关的信息的,是Golang的一个基础package。

    go/build.Context包含有支持构建的上下文信息,会保存一些Golang的基本信息,以及其他和构建相关的信息。

  3. go/build中的Package对象

    go/build.Package对象位于文件go/build/build.go中。这个Package对象用来描述一个目录中的package(Golang的每个目录都是一个package)。当上面提到的loadImport函数调用buildContext.Import()时,它得到的就是一个go/build.Package

现在回过头来看loadImport的流程。 上面已经列出了这个流程的主要步骤,这里的关键点在于步骤4和步骤5。步骤4的buildContext.Import(path, srcDir, mode)是把这个path目录中的package的信息收集起来;而步骤5则是载入这个package相关的依赖。这两个步骤的内容就非常多了,在后面的文章独立说明。

总结

我们现在知道了go build命令的入口,并且知道当要构建一个package的时候,要先载入这个package的信息。这个主要是在loadPackage这个函数中完成的,载入的是通过调用loadImport函数进行的,要分别载入这个package自己的信息,然后再载入它的依赖信息。这个过程说明了,载入一个命令行指定的package和载入一个import语句指定的package是差不多的。

下一篇文章会说明buildConext.Import内部的逻辑。


知识共享许可协议本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。