Golang源码阅读笔记 -- go build(1)
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.go的runBuild(cmd *Command, args []string)
函数。该函数会在cmd/go/main.go的main()函数中被调用。调用前,会先解析所有的命令行参数,解析的参数会设置到cmd/go/build.go文件的全局变量中,比如buildA
、buildN
等。其他的非参数部分,就会通过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的操作。分为两种情况:
- 如果
args
中都是文件名,比如go build -a a.go b.go
,那么会调用goFilesPackage(args)
来执行载入package的操作。 - 否则,就是
go build -a goexample/hello goexample/foo
这种情况。
第一种情况我们先跳过,回头再来看。先来看第二种情况,也就是更常见的情况。我们先限制命令行参数只传递了一个package goexample/hello,然后来看下代码:
args = importPaths(args)
,该函数处理命令行传递进来的packages的特殊情况,比如Windows系统、路径中多余的部分、std/cmd/all这些保留名称、扩展符…等,然后返回所有所有需要处理packages。此时返回的每个package都可以认为是一个ImportPath。因为我们举的例子goexample/hello*都不在特殊情况里面,所以它会原样返回。- 针对args中的每个ImportPath,调用
loadPackage(arg, &stk)
来载入这个package。其中stk
参数是一个表示依赖深度的栈,后面再仔细说明。在这一步,就得到了所需的Package
对象。 - 最后,调用
computeStale()
函数,该函数会判断一个package是否需要重新构建。如果go build
传递了-a
参数,那么所有的packages都要重新构建。
从上面的流程可以看出,载入一个package的主要工作都是在loadPackage()
函数中完成的。
loadPackage
函数
loadPackage(arg string, stk *importStack) *Package
位于cmd/go/pkg.go文件中,它载入一个命令行参数指定的package。stk
参数是用于记录import路径的一个栈。
该函数的流程分为4个部分:
- 判断
arg
是否是local import。Local Import表是一个ImportPath是”.”、”..”、”./”开头后者是”../”开头。是的话,就对arg
进行特殊处理。 - 判断
arg
是否以cmd/开头且cmd/之后没有/。是的话表示构建的是一个go命令。会在这个分支里执行载入,然后返回。 - 如果构建的不是一个go命令,那么就是一个package。会先处理一下local import一个standard package的情况,也就是说使用local import的方式指定一个standard package的情况。standard package是指GOROOT中的那些package。
- 最后,调用
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
中的主要流程。这个函数中还考虑了vendor和internal的情况,我们先跳过这些,只看主要步骤:
stk.push(path)
对path
进行入栈处理,这个栈的作用上面已经讲过。- 通过
packageCache
全局变量,判断这个path
是否已经被处理过了,如果已经处理过,就直接进行重用,调用reusePackage(p, stk)
。 - 生产一个新的
Package
对象p
,并且加入packageCache
。 - 调用
bp, err := buildContext.Import(path, srcDir, buildMode)
来获得这个path
对应的go/build.Package
对象。 - 调用
p.load(stk, bp, err)
把这个build.Package
对象载入到Package
对象p
中。 - 返回
p
。
上面这个流程里有几个新的东西,我们先来看一下。
-
reusePackage
函数,这个函数的主要作用是判断是否存在循环应用的情况。在后面的文章会讲解判断方法。 -
buildContext
cmd/go/build.go文件中的
buildContext
变量是用来保存构建上下文的全局变量,默认指向go/build.Default
。注:
go/build
这个package是用来收集一个go package有关的信息的,是Golang的一个基础package。go/build.Context
包含有支持构建的上下文信息,会保存一些Golang的基本信息,以及其他和构建相关的信息。 -
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
内部的逻辑。