golang标准库中的go/build包是用来收集包的一些信息的,例如获取包的源码位置。用来学习golang的交互式教程tour就是使用了go/build定位tour源代码位置,从而在运行时使用其中的文件。但是go/build是有一些使用限制的,主要是在某些情况下无法定位go module的源码位置,那么还有哪些方法可以根据包的importpath获取到包的源代码位置呢,本文详细进行探讨。
起因
golang 1.12.9,设置了GO111MODULE=on,也就是显式开启go module。
按照tour的使用方法,首先:
1 | go get golang.org/x/tour |
然后到GOPATH/bin目录下运行tour程序,报如下错误:
Couldn’t find tour files: could not find go-tour content; check $GOROOT and $GOPATH
而在设置GO111MODULE=on之前一切都是正常的。
通过阅读tour的源代码发现它运行时需要使用源代码目录下的文件,这是通过标准库go/build.Context.Import来定位源代码的路径的,具体代码如下,见local.go文件:
1 | const ( |
p.Dir即为包对应的源代码路径,其中关键代码是调用了go/build.Context.Import:
1 | func (ctxt *Context) Import(path string, srcDir string, mode ImportMode) |
分析go/build.Context.Import的源代码发现,当srcDir为空时总是无法获取go module包信息。在进一步分析原因之前我们先了解一下go module相关知识。
go module
go module是golang 1.11引入的包管理机制,可以通过GO111MODULE环境变量控制是否启用,有三个值:
- off:关闭go module
- on:总是使用go module
- auto:默认值,如果当前工程包含go.mod文件则启用 go module,否则使用旧的GOPATH和vendor机制
当启用go module时,一个变化是下载的依赖包不再放在GOPATH/src目录下,而是放在GOPATH/pkg/mod(未来可能迁移到GOCACHE/mod)下,并且目录带有版本号,以tour为例,启用go module之前源码位置为:
1 | GOPATH/src/golang.org/x/tour |
启用之后变为:
1 | GOPATH/pkg/mod/golang.org/x/tour@v0.0.0-20191002171047-6bb846ce41cd |
module和package
module和package是集合和元素的关系,一个module可以包含0个、1个或多个package,module的Path和其中package的ImportPath前缀一定是相同的,例如golang.org/x/tools是一个module,其中的golang.org/x/tools/go/packages是一个package。
import一定是package,而不是module,当前module中执行go mod tidy命令可以根据源码中的import补齐go.mod中缺失的module以及删除多余的module。
回到问题的原因
go/build在最初没有引入go module之前,总是从GOROOT/src和GOPATH/src下寻找包的,这时是没有问题的,当启用go module后,包缓存变到了GOPATH/pkg/mod下,这时就找不到了,case 26504提出了这个问题并做了修正,但是出于效率的考虑,只有在特定的情况下才会针对go module进行处理,然后通过执行go list命令实现的。特定的情况是:
- the AllowBinary and IgnoreVendor flags are not set,
- Context describes the local file system (all the helper functions are nil),
- srcDir is outside GOPATH/src (or GO111MODULE=on),
- there’s a go.mod in srcDir or above,
- the release tags are unmodified from Default.Context,
- and path does not name a standard library package.
当srcDir为空的时候,是没有办法判断上述条件3和4的,就会直接返回失败,所以这时go/build仍然无法获得相应module的信息,tour在调用go/build.Context.Import时传入的srcDir正是为空。
详解go/build.Context.Import过程
1 | func (ctxt *Context) Import(path string, srcDir string, mode ImportMode) |
- path:模块的Path或包的ImportPath
- srcDir:当前工程的源码路径,下面会讲在哪些情况会用到
- mode:FindOnly,AllowBinary,ImportComment,IgnoreVendor
Import的主要过程如下:
-
如果为本地包,也就是path为相对路径,相对的正是srcDir,这时先根据srcDir得到绝对路径,然后根据此绝对路径依次在GOROOT/src和GOPATH/src中寻找,绝对路径必须为其子目录,找到第一个符合即Import返回找到包,这种情况下srcDir不能为空,最后p.Dir的结果是Join(srcDir, path)
-
如果非本地包:
-
首先针对module尝试使用go list命令获取包信息,这时有一系列限制条件,具体见上文,其中需要判断srcDir不在GOROOT/src和GOPATH/src下且其路径上有go.mod文件,所以srcDir为空则直接寻找失败继续下一步,否则执行go list,命令的当前路径为srcDir,srcDir必须为module的路径,类似如下命令,如果成功则Import返回找到包,最后p.Dir的结果是go list返回的Dir
1
go list -f {{.Dir}} path
-
然后尝试在vendor目录中寻找,这时srcDir也不能为空,使用srcDir依次在GOROOT/src和GOPATH/src中寻找,srcDir必须为其子目录,找到第一个符合即Import返回找到包,最后p.Dir的结果是Join(Abs(srcDir), “vendor”, path),否则下一步
-
然后尝试在GOROOT/src中寻找,成功则Import返回找到包,p.Dir的结果为Join(GOROOT, “src”, path),否则下一步
-
然后尝试依次在所有GOPATH/src中寻找,找到第一个符合即Import返回找到包,p.Dir的结果为Join(GOPATH, “src”, path)
-
-
以上过程都没有定位成功则Import返回失败
根据上述过程,srcDir在下述三种情况下起到重要作用,如果为空则会导致找不到包:
- 本地包,此时path是相对srcDir的
- module,此时还需要srcDir不能在GOROOT/src或GOPATH/src中,且路径上包含go.mod文件,srcDir需要为此module的路径
- vendor目录中的包,此时path在Join(srcDir, “vendor”)目录下
定位包源码位置的方法梳理
上述go/build在定位包源码位置时存在种种限制,那么是否还有其他的方法,答案是肯定的,共有如下方法:
它们之间是有依赖关系的,go/build对模块的支持需要调用go list;golang.org/x/tools/go/packages默认是通过go list来获取包或模块信息的,下面具体说明其使用方法。
go list
1 | go list [-f format] [-json] [-m] [list flags] [build flags] [packages] |
go list用于列出当前目录主包或模块及其依赖。当GO111MODULE=on且没有指定-m时,如果当前目录找不到,还会从GOPROXY指定地址下载,此时不能使用通配符。
-
packages:当指定-m时为模块的path列表,否则为包的importpath列表;为空时只列出主模块或主包,不为空时则列出匹配的,可以用…通配符;有一些特殊的path,对于模块可以使用all,表示列出当前主模块及所有依赖模块,对于包有all/std/cmd几个,all表示列出GOPATH中所有包,std列出标准库的包,cmd会列出go仓库中的命令及内部库;对于模块还可以使用模块查询,例如可以通过path@version带上版本;更多内容可参考go help packages和go help modules
-
-f format:可以使用go模板定制输出内容
-
模块有如下字段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17type Module struct {
Path string // module path
Version string // module version
Versions []string // available module versions (with -versions)
Replace *Module // replaced by this module
Time *time.Time // time version was created
Update *Module // available update, if any (with -u)
Main bool // is this the main module?
Indirect bool // is this module only an indirect dependency of main module?
Dir string // directory holding files for this module, if any
GoMod string // path to go.mod file for this module, if any
Error *ModuleError // error loading module
}
type ModuleError struct {
Err string // the error itself
} -
包有如下字段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58type Package struct {
Dir string // directory containing package sources
ImportPath string // import path of package in dir
ImportComment string // path in import comment on package statement
Name string // package name
Doc string // package documentation string
Target string // install path
Shlib string // the shared library that contains this package (only set when -linkshared)
Goroot bool // is this package in the Go root?
Standard bool // is this package part of the standard Go library?
Stale bool // would 'go install' do anything for this package?
StaleReason string // explanation for Stale==true
Root string // Go root or Go path dir containing this package
ConflictDir string // this directory shadows Dir in $GOPATH
BinaryOnly bool // binary-only package: cannot be recompiled from sources
ForTest string // package is only for use in named test
Export string // file containing export data (when using -export)
Module *Module // info about package's containing module, if any (can be nil)
Match []string // command-line patterns matching this package
DepOnly bool // package is only a dependency, not explicitly listed
// Source files
GoFiles []string // .go source files (excluding CgoFiles, TestGoFiles, XTestGoFiles)
CgoFiles []string // .go source files that import "C"
CompiledGoFiles []string // .go files presented to compiler (when using -compiled)
IgnoredGoFiles []string // .go source files ignored due to build constraints
CFiles []string // .c source files
CXXFiles []string // .cc, .cxx and .cpp source files
MFiles []string // .m source files
HFiles []string // .h, .hh, .hpp and .hxx source files
FFiles []string // .f, .F, .for and .f90 Fortran source files
SFiles []string // .s source files
SwigFiles []string // .swig files
SwigCXXFiles []string // .swigcxx files
SysoFiles []string // .syso object files to add to archive
TestGoFiles []string // _test.go files in package
XTestGoFiles []string // _test.go files outside package
// Cgo directives
CgoCFLAGS []string // cgo: flags for C compiler
CgoCPPFLAGS []string // cgo: flags for C preprocessor
CgoCXXFLAGS []string // cgo: flags for C++ compiler
CgoFFLAGS []string // cgo: flags for Fortran compiler
CgoLDFLAGS []string // cgo: flags for linker
CgoPkgConfig []string // cgo: pkg-config names
// Dependency information
Imports []string // import paths used by this package
ImportMap map[string]string // map from source import to ImportPath (identity entries omitted)
Deps []string // all (recursively) imported dependencies
TestImports []string // imports from TestGoFiles
XTestImports []string // imports from XTestGoFiles
// Error information
Incomplete bool // this package or a dependency has an error
Error *PackageError // error loading package
DepsErrors []*PackageError // errors loading dependencies
}
-
下面通过一些例子来说明其使用,假设当前目录为tour源码根目录:
-
列出主模块目录
1
go lis -m -f {{.Dir}}
-
列出主模块和所有依赖模块目录
1
2go list -m -f {{.Dir}} all
go list -m -f {{.Dir}} ...上述两种效果是一样的。
-
列出符匹配条件的模块目录
1
go list -m -f {{.Dir}} golang.org/x/...
-
列出主包目录
1
go list -f {{.Dir}}
-
列出主包和所有依赖包目录
1
go list -f {{.Dir}} ...
-
列出GOPATH所有包目录
1
go list -f {{.Dir}} all
-
列出符匹配条件的包目录
1
go list -f {{.Dir}} golang.org/x/...
golang.org/x/tools/go/packages
对于本文一开始提到的tour的问题,可以修改findRoot方法,用packages.Load最后再尝试一次定位golang.org/x/tour的源码位置。
1 | func findRoot() (string, error) { |
packages.Load最后通过调用go list获得包信息,命令如下:
1 | go list -e -json -compiled -test=false -export=false -deps=false -find=true -- golang.org/x/tour |
因为packages.Config中我们没有指定Dir(这个Dir会作为上述命令执行的目录),所以上述命令使用运行tour的当前目录,也就是WORKSPACE/bin作为当前目录,这时当前目录是找不到包的,但是因为开启了GO111MODULE=on,所以上述命令会尝试从GOPROXY下载golang.org/x/tour模块,如果此时无法联网,最终也找不到包。
packages.Load返回的包信息可使用字段如下:
1 | // A Package describes a loaded Go package. |
其中并没有包的Dir,所以只有从GoFiles中获取Dir,如果GoFiles为空,这时就无法获取到包的Dir,这似乎是一个问题,建议golang.org/x/tools/go/packages能够从go list返回这个字段。
go/build
go/build也是tour所采用的方法,和上述方法的不同之处在于,这个包一次只处理一个包,传入的path不能有通配符,必须是一个明确的path,而上述两种方法是可以批量的。也因此go/build比较注重效率,在case 26504中已经做了阐述。这个包的使用在传入srcDir参数是也有一些讲究,上文已经详细探讨。
总结
目前并没有完美的方法从包的importpath定位到源码目录,虽然golang.org/x/tools/go/packages或go list可以全面的支持包或模块的检索,但是在不知道源码位置的时候,必须启用GO111MODULE=on并联网,而且这时获取到的版本并不一定是需要的版本,而go/build在GO111MODULE=on时对模块中的包是无法获取源码位置的。
综上对于模块中的包目前暂且可用的方案也只有golang.org/x/tools/go/packages或go list。对于非模块中的包最好用go/build。
模块中的包,存在版本号的问题,无法像非模块包那样直接从GOROOT和GOPATH中定位即可,你无法用相同的方法在GOPATH/pkg/mod中定位,因为可能存在多个版本的包,这是难以仅根据包的importpath定位源码位置的问题根源。