上一篇文章go build(1)中,我们已经来到了cmd/go/pkg.go文件的loadImport函数中,在这个函数里,会调用buildContext.Import()方法来载入一个package的信息。本文会深入的看一下这个buildContext.Import()中的逻辑。

go/build.ContextImport方法

这个方法位于go/build/build.go文件中,是Context对象的一个导出接口。该接口的声明是

(ctx *Context) Import(path string, srcDir string, mode ImportMode) (*Package, error)

它的参数含义如下:

  • path 表示这个package的ImportPath。
  • srcDirpath是一个local path的时候,相对路径是以srcDir开始的。
  • mode 是用来控制Import方法的行为的。目前有三个值:
    • FindOnly 只查找package所在的目录,不读取其中的源码的内容
    • ImportComment 解析import comment。Golang允许在import语句之后跟随一个有含义的comment,具体的功能见http://golang.org/s/go14customimport
    • IgnoreVendor 忽略Vendor目录的搜索。

Import方法很长,这里还是只描述最常见情况下的代码路径,也就是参数path的值是goexample/hello这样的形式,且没有vendor目录,而srcDir的值是命令执行时的当前工作目录。

Import方法工作就是生产一个go/build.Package对象(在本文中我们简称为Package对象,在上一篇文章中还有一个cmd/go.Package对象,不要弄混了)。Import方法的逻辑主要分为几块,我们分别来看。

设置构建结果的目标路径

当要构建一个package的时候,就要知道构建结果的存放位置,包括最终结果和中间结果。这里首先会设置设置两个变量的值。

pkgtargetroot,这个是存放结果的根目录,这个目录下会存放很多不同的package的构建结果。在Golang的环境中,你可以在GOPATH和GOROOT目录下分别找到pkg/目录,这个目录就是存放构建结果的地方,其中,GOROOT下的pkg目录中存放的是Golang的标准库构建的结果,都是以arcihve的文件格式存放的,当你的代码引用了标准库时,标准库可以不用重新编译,直接进行链接

pkgtargetroot的值是由pkg/前缀、操作系统、架构和suffix组成的。最常用的情况下,使用gc编译器(也就是Golang官方编译器,另一种是gccgo),没有suffix,是linux系统和x86_64架构,所以pkgtargetroot的值是pkg/linux_amd64。这个目录就这个最常用的情况下,package存放构建结果的地方。

pkgtargetroot下还要为每个package建立单独的子目录用于存放构建结果。这里是否有子目录,完全取决于package的ImportPath,也就是Import方法的path参数。比如我们的goexample/hello,最终生成的构建目标文件路径为pkg/linux_amd64/goexample/hello.a

最后,当Import返回前,会把这些路径设置到Package对象中。其中PkgObj成员用来存放package构建结果的完整存放路径,类似GOPATH/pkg/linux_amd64/goexample/hello.a

查找package的位置

接下来,Import方法会根据是否是local import来查找package的位置,这里我们只看非local import的情况。按照下面的顺序来查找:

  1. 如果允许查找vendor目录,就先查找vendor目录。这里我们先忽略。
  2. 如果GOROOT不为空,那么就先查找GOROOT的src目录下是否存在path的值这个目录,就是GOROOT/src/goexample/hello这个目录,找到则跳转到Found标签。
  3. 遍历所有的GOPATH,进行和上面一样的查找,找到则跳转到Found标签。
  4. 如果上面都找不到,那么说明这个package不存在,也就是你提供的ImportPath指向了一个不存在的目录,那么就在这里返回失败。

找到了之后,设置package对象的Root为包含这个package的GOROOT或者GOPATH的路径,设置package对象的Dir为这个package的完整路径(绝对路径)。

Found标签下的处理

现在,已经找到了package所在的位置。接下来就要处理package的内容。在Found标签开始的地方,先读取package目录下的所有文件,然后针对每个文件进行一段复杂的处理。所有的文件都处理过后,就收集了这个package的绝大部分信息,然后再进行一些汇总处理。这里的重点是针对每个文件的处理过程。

针对每个文件的处理过程只处理于package目录下的文件,跳过子目录,原因是Golang把每个目录都当成一个独立的package。这个处理过程大概分为4个部分。

  1. 匹配文件 你会先看到一行代码match, data, filename, err := ctxt.matchFile(p.Dir, name, true, allTags, &p.BinaryOnly)。这个matchFile方法的功能是判断当前文件是否需要参与到构建中来。这个判断条件有两个:
    1. 根据文件名中的os和arch信息来判断是否符合。Golang支持的文件名格式有六种:
      1. name_$(GOOS).*
      2. name_$(GOARCH).*
      3. name_$(GOOS)_$(GOARCH).*
      4. name_$(GOOS)_test.*
      5. name_$(GOARCH)_test.*
      6. name_$(GOOS)_$(GOARCH)_test.*
    2. 根据文件投的注释中的build tag来判断是否符合。这个可以查看文档Package buld

    上面看到matchFile文件返回了4个参数,第一个参数match表示这个文件是否需要参与到构建中,第二个参数data包含了这个文件从开头到import语句结束的内容,第三个参数是这个文件的绝对路径名。

    如果这里得到的matchfalse,说明文件不需要参与构建,可以开始处理下一个文件。

  2. 忽略非Golang源码的文件

    对于扩展名不是.go的文件,会把文件名记录到对应的列表中,然后开始处理下一个文件。

  3. 解析文件内容

    严格的说,是解析从文件开头到import语句结束这部分的内容,这部分内容是我们在上面调用matchFile方法时获得的。 解析过程是执行代码:

     pf, err := parser.ParseFile(fset, filename, data, parser.ImportsOnly|parser.ParseComments)
    

    parsergo/parser这个package,一个专门用于解析Golang源码的package。这个会在专门的文章说阐述,不在本文中展开讲解。

    parser.ParseFile函数的最后一个参数我们执行了两个mode:ImportsOnlyParseComments,这个是因为我们传递进去的内容只包含了从文件开头到import语句结束的地方,ImportsOnly表示解析到imports语句结束的地方就返回,而ParseComments表示会解析注释,如果文件开头存在注释,那么就可以得到一个文件doc。

    parser.ParseFile函数的第一个参数fset是一个go/token.FileSet对象,用来表示多个Golang源码文件构成的集合,主要是用来在解析源码的过程中快速的处理文件位置。

    parser.ParseFile函数的返回值是一个go/ast.File对象,这个是将一个文件的内容进行语法解析后得到的对象。ast的是Abstract Syntax Tree的缩写,即抽象语法树

  4. 处理解析后的文件

    解析完这个文件后,就可以根据得到的go/ast.File对象继续进行处理:

    • 如果package名为documentation时的情况
    • 处理package名xxx_test的情况,也就是允许同一个目录中包含有一个名为xxx的package,以及它的黑盒测试package,名为xxx_test
    • 检查一个目录下的文件是否声明了多个不同的package名称(上面的_test情况除外)
    • 处理import comment的内容
    • 查找并记录这个文件中import的package
    • 记录cgo的信息

针对每个文件的处理流程结束后,就把记录的信息设置到要返回的Package对象中。经过这些处理,我们得到的Package对象中记录了这个package的基本信息,包括这个package中包含哪些文件、package名称以及这个package的依赖等。

小结

经过上面这些处理,当go/build.ContextImport方法成功返回时,会给调用者一个go/build.Package对象,这个对象会包含这个package的基本信息,以及这个package的依赖信息。


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