0%

golang中如何通过包的importpath定位源码目录

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const (
basePkg = "golang.org/x/tour"
socketPath = "/socket"
)

func findRoot() (string, error) {
ctx := build.Default
p, err := ctx.Import(basePkg, "", build.FindOnly)
if err == nil && isRoot(p.Dir) {
return p.Dir, nil
}
tourRoot := filepath.Join(runtime.GOROOT(), "misc", "tour")
ctx.GOPATH = tourRoot
p, err = ctx.Import(basePkg, "", build.FindOnly)
if err == nil && isRoot(tourRoot) {
gopath = tourRoot
return tourRoot, nil
}
return "", fmt.Errorf("could not find go-tour content; check $GOROOT and $GOPATH")
}

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命令实现的。特定的情况是:

  1. the AllowBinary and IgnoreVendor flags are not set,
  2. Context describes the local file system (all the helper functions are nil),
  3. srcDir is outside GOPATH/src (or GO111MODULE=on),
  4. there’s a go.mod in srcDir or above,
  5. the release tags are unmodified from Default.Context,
  6. 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的主要过程如下:

  1. 如果为本地包,也就是path为相对路径,相对的正是srcDir,这时先根据srcDir得到绝对路径,然后根据此绝对路径依次在GOROOT/src和GOPATH/src中寻找,绝对路径必须为其子目录,找到第一个符合即Import返回找到包,这种情况下srcDir不能为空,最后p.Dir的结果是Join(srcDir, path)

  2. 如果非本地包:

    1. 首先针对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
    2. 然后尝试在vendor目录中寻找,这时srcDir也不能为空,使用srcDir依次在GOROOT/src和GOPATH/src中寻找,srcDir必须为其子目录,找到第一个符合即Import返回找到包,最后p.Dir的结果是Join(Abs(srcDir), “vendor”, path),否则下一步

    3. 然后尝试在GOROOT/src中寻找,成功则Import返回找到包,p.Dir的结果为Join(GOROOT, “src”, path),否则下一步

    4. 然后尝试依次在所有GOPATH/src中寻找,找到第一个符合即Import返回找到包,p.Dir的结果为Join(GOPATH, “src”, path)

  3. 以上过程都没有定位成功则Import返回失败

根据上述过程,srcDir在下述三种情况下起到重要作用,如果为空则会导致找不到包:

  1. 本地包,此时path是相对srcDir的
  2. module,此时还需要srcDir不能在GOROOT/src或GOPATH/src中,且路径上包含go.mod文件,srcDir需要为此module的路径
  3. vendor目录中的包,此时path在Join(srcDir, “vendor”)目录下

定位包源码位置的方法梳理

上述go/build在定位包源码位置时存在种种限制,那么是否还有其他的方法,答案是肯定的,共有如下方法:
1570634569609

它们之间是有依赖关系的,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
      17
      type 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
      58
      type 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
    2
    go 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func findRoot() (string, error) {


cfg := &packages.Config{Mode: packages.LoadFiles}
pkgs, _ := packages.Load(cfg, basePkg)
for _, pkg := range pkgs {
for _, goFile := range pkg.GoFiles {
root := filepath.Dir(goFile)
if isRoot(root) {
return root, nil
}
}
}
return "", fmt.Errorf("could not find go-tour content; check $GOROOT and $GOPATH")
}

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
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
58
59
60
61
62
63
64
65
66
67
// A Package describes a loaded Go package.
type Package struct {
// ID is a unique identifier for a package,
// in a syntax provided by the underlying build system.
//
// Because the syntax varies based on the build system,
// clients should treat IDs as opaque and not attempt to
// interpret them.
ID string

// Name is the package name as it appears in the package source code.
Name string

// PkgPath is the package path as used by the go/types package.
PkgPath string

// Errors contains any errors encountered querying the metadata
// of the package, or while parsing or type-checking its files.
Errors []Error

// GoFiles lists the absolute file paths of the package's Go source files.
GoFiles []string

// CompiledGoFiles lists the absolute file paths of the package's source
// files that were presented to the compiler.
// This may differ from GoFiles if files are processed before compilation.
CompiledGoFiles []string

// OtherFiles lists the absolute file paths of the package's non-Go source files,
// including assembly, C, C++, Fortran, Objective-C, SWIG, and so on.
OtherFiles []string

// ExportFile is the absolute path to a file containing type
// information for the package as provided by the build system.
ExportFile string

// Imports maps import paths appearing in the package's Go source files
// to corresponding loaded Packages.
Imports map[string]*Package

// Types provides type information for the package.
// Modes LoadTypes and above set this field for packages matching the
// patterns; type information for dependencies may be missing or incomplete.
// Mode LoadAllSyntax sets this field for all packages, including dependencies.
Types *types.Package

// Fset provides position information for Types, TypesInfo, and Syntax.
// It is set only when Types is set.
Fset *token.FileSet

// IllTyped indicates whether the package or any dependency contains errors.
// It is set only when Types is set.
IllTyped bool

// Syntax is the package's syntax trees, for the files listed in CompiledGoFiles.
//
// Mode LoadSyntax sets this field for packages matching the patterns.
// Mode LoadAllSyntax sets this field for all packages, including dependencies.
Syntax []*ast.File

// TypesInfo provides type information about the package's syntax trees.
// It is set only when Syntax is set.
TypesInfo *types.Info

// TypesSizes provides the effective size function for types in TypesInfo.
TypesSizes types.Sizes
}

其中并没有包的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定位源码位置的问题根源。

-------------本文结束感谢您的阅读-------------