Golang源码阅读笔记 -- go build(3)
在第一篇文章go build(1)中,我们提到了cmd/go/pkg.go文件中的loadImport
函数。该函数有两个主要的步骤,一个是调用buildContext.Import()
方法来获得要构建的package的基本信息,这个我们在上一篇文章go build(2)中已经说明了其内部逻辑,另外一个调用是p.load(stk, bp, err)
,也就是载入这个package的依赖。本文要看的是第二个步骤的具体过程。
cmd/go.Package
对象的load
方法
该方法位于cmd/go/pkg.go文件中。
调用说明
在loadImport
函数中执行的代码是p.load(stk, bp, err)
:
p
是cmd/go.Package
对象,这个是表示一个要构建的package。相对的go/build.Package
表示一个被import的package。前者包含的信息更多。stk
是一个表示依赖深度的栈,用于记录import路径的一个栈。通过栈的push和pop操作,不断的记录import的路径,栈顶是当前正在处理的package,栈顶下的那个package则import了栈顶package。bp
是buildContext.Import
方法返回的对象,是一个go/build.Package
对象,包含了要构建的package的基本信息。
load
方法的内部逻辑
强调一下,这里我们还是以最常见的import path形式,类似goexample/hello这样,来看load
方法的内部逻辑。
拷贝信息
首先执行p.copyBuild(bp)
将go/build.Package
对象的内容拷贝到p
,也就是cmd/go.Package
中,也会把bp
赋值给p.build
。
构建结果存放位置的处理
区分两个情况:
- package的名称是main,也就是构建结果是一个可执行程序。这个时候构建结果的存放目录是在一个bin目录下,最常见的情况就是GOPATH/bin目录,输出的文件名是package的目录名。
- 如果是非main的package,那么一般使用
p.build.PkgObj
这个路径作为目标路径。这个路径在上一篇文章中有描述了生成过程,类似GOPATH/pkg/linux_amd64/goexample/hello.a。
依赖的预处理
之前已经得到了要构建的package的依赖,保存在p.build.Imports
中,又被拷贝到p.Imports
中。这些只是源码中写的import
语句的依赖,构建过程还需要加上其他的内部依赖。对于最常见的情况,还需要把runtime增加到依赖中。其他的情况会根据是否有cgo、架构平台、是否是标准库以及命令行参数等条件来增加相应的依赖。经过预处理的依赖存放在变量importPaths
中。
文件的预处理
将这个package中的go文件和非go文件都生成绝对路径,保存到相应的变量中并按照文件名排序。
检查package中的文件名是否有重名,这个要求是忽略大小的情况下也不能重名。
处理依赖
接下来,load
方法开始处理依赖,也就是importPaths
变量中的package。这里会遍历每个ImportPath,然后依次处理。
- 首先,跳过ImportPath为C的package,因为这是cgo的package,不属于依赖。
- 执行
p1 := loadImport(path, p.Dir, p, stk, p.build.ImportPos[path], useVendor)
。看到loadImport
函数是不是觉得很熟悉?没错,这个就是在文章go build(1)中提到的loadImport
函数,也就是调用了我们正在讲解的这个cmd/go.Package
对象的load
方法。所以,这里是一个递归调用,不过同样path的package只会收集一次信息,之后再遇到会直接从cache返回,这个在文章go build(1)中已经提到过了。 - 判断一下import进来的package的名称是否是main,是的话就报错。
- 把
p1
加入到deps
这个map中,这样deps
就是一个包含所有packagep
的依赖的map。同样,也把p1
加入到imports
这个slice中,这样imports
就是一个包含所有packagep
的依赖的slice。 - 如果
p1.Incomplete
为true
,那么也设置p.Incomplete
为true
。Incomplete
表示在载入这个package或者它的依赖时出现错误。
经过上面的处理,就已经递归的载入了package p
的所有依赖,这些依赖的相关信息会被记录到p
中,p.imports = imports
,p.deps
会是排序过的deps
。
计算BuildID
对于编译一个package而言,buildID是根据这个package的所有文件的文件名,和这个package的所有依赖的ImportPath以及依赖的buildID,计算出来的一个SHA-1值,主要是用来判断一个package是否需要重新构建。这个在后面会讲到。
小结
完成上面这些步骤后,这个package的依赖也就都载入成功了,并且buildID也已经计算完成。
reusePackage
函数
reusePackage(p *Package, stk *importStack)
函数位于文件cmd/go/pkg.go文件中。我们已经知道,这个函数会在loadImport
中被调用,用来判断是否有循环应用的情况。现在我们来仔细看一下这个逻辑。
首先,看一下loadImport
中的代码:
func loadImport(path, srcDir string, parent *Package, stk *importStack, importPos []token.Position, mode int) *Package {
...
importPath := path
...
if p := packageCache[importPath]; p != nil {
...
return reusePackage(p, stk)
}
p := new(Package)
p.local = isLocal
p.Importpath = importPath
packageCache[importPath] = p
...
}
从上面代码可以看出,在发现现在要import的path已经被处理过了之后,就可以直接返回cache中的cmd/go.Package
对象,但是返回前要先调用reusePackage
函数检查是否循环引用。如果这个path不在cache中,那么创建新的package之后,立刻加入到cache。
接下来看一下reusePackage
中是如何检查循环引用的:
func reusePackage(p *Package, stk *importStack) *Package {
if p.imports == nil {
// import cycle
}
...
return p
}
上一节中,我们看到了,p.load
这个方法在收集了所有的依赖的信息后,才会设置自己的p.imports
变量。所以,只要p.imports
这个变量还是空的,就说明这个package还没有处理完依赖,所以只要在处理依赖的过程中再次遇到自己,就会触发reusePackage
函数的调用,就会看到p.imports == nil
。
回到packagesAndErrors
函数
现在回到文章go build(1)中的packagesAndErrors
函数。我们回顾一下到目前为止走过的流程:
packagesAndErrors()
loadPackage()
loadImport()
reusePackage()
buildContext.Import()
p.load()
computeStale()
到目前为止,我们已经分析了从loadPackage()
函数开始往下的流程。现在,在packagesAndErrors
函数中,我们通过调用loadPackage
函数,得到了一个cmd/go.Package
对象pkg
。下面就开始调用computeStale(pkg)
。
computeStale
函数
这个函数会判断一个package是否需要重新构建,也就是是否stale。如果需要,就设置pkg.Stale
为true
。判断的工作是由isStale(p *Package) (bool, string)
函数完成的,这个函数位于cmd/go/pkg.go文件中。
判断stale的逻辑在isStale
函数的注释中写的非常清楚,包括演变过程,主要是基于buildID和文件修改时间来判断的。
- buildID的方法如下:编译输出时,将这个package的buildID记录到目标文件中,在
isStale
函数中,会读取目标文件中保存的buildID,如果和现在package中计算出来的buildID不同,那么就需要重新构建。 - 基于文件修改时间的方法如下:只要构建package有关的文件中,有一个文件的修改时间比目标文件晚,那么就需要重新构建。
上面这个是最基本的方法介绍,isStale
函数中还考虑了命令行参数,以及文件是否存在等情况。
小结
packagesAndErrors
函数调用完computeStale
之后,就返回所获取到的packages对象。
因为packagesForBuild
函数是调用了packagesAndErrors
函数来获得packages,如果获取的过程没有错误,那么packagesForBuild
就返回获取到的packages。我们又回到了runBuild
函数中。