第一篇文章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)

  • pcmd/go.Package对象,这个是表示一个要构建的package。相对的go/build.Package表示一个被import的package。前者包含的信息更多。
  • stk是一个表示依赖深度的栈,用于记录import路径的一个栈。通过栈的pushpop操作,不断的记录import的路径,栈顶是当前正在处理的package,栈顶下的那个package则import了栈顶package。
  • bpbuildContext.Import方法返回的对象,是一个go/build.Package对象,包含了要构建的package的基本信息。

load方法的内部逻辑

强调一下,这里我们还是以最常见的import path形式,类似goexample/hello这样,来看load方法的内部逻辑。

拷贝信息

首先执行p.copyBuild(bp)go/build.Package对象的内容拷贝到p,也就是cmd/go.Package中,也会把bp赋值给p.build

构建结果存放位置的处理

区分两个情况:

  1. package的名称是main,也就是构建结果是一个可执行程序。这个时候构建结果的存放目录是在一个bin目录下,最常见的情况就是GOPATH/bin目录,输出的文件名是package的目录名。
  2. 如果是非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,然后依次处理。

  1. 首先,跳过ImportPath为C的package,因为这是cgo的package,不属于依赖。
  2. 执行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)中已经提到过了。
  3. 判断一下import进来的package的名称是否是main,是的话就报错。
  4. p1加入到deps这个map中,这样deps就是一个包含所有package p的依赖的map。同样,也把p1加入到imports这个slice中,这样imports就是一个包含所有package p的依赖的slice。
  5. 如果p1.Incompletetrue,那么也设置p.IncompletetrueIncomplete表示在载入这个package或者它的依赖时出现错误。

经过上面的处理,就已经递归的载入了package p的所有依赖,这些依赖的相关信息会被记录到p中,p.imports = importsp.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.Staletrue。判断的工作是由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函数中。


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