使用vendor管理Golang项目依赖

The Vendor Tool for Go

go get -u github.com/kardianos/govendor

New users please read theFAQ

Package developers should read the developer guide.

For a high level overview read the whitepaper

Uses the go1.5+ vendor folder. Multiple workflows supported, single tool.

  • Copy existing dependencies from$GOPATH with govendor add/update .
  • If you ignore vendor/*/ , restore dependencies with govendor sync .
  • Pull in new dependencies or update existing dependencies directly from 
    remotes with govendor fetch .
  • Migrate from legacy systems with govendor migrate .
  • SupportsLinux, OS X, Windows, probably all others.
  • Supports git, hg, svn, bzr(must be installed an on the PATH).

Notes

  • The project must be within a $GOPATH.
  • If using go1.5, ensure you set GO15VENDOREXPERIMENT=1 .

Quick Start, also see theFAQ

Setup your project.

cd "my project in GOPATH"

govendor init

Add existing GOPATH files to vendor.

govendor add +external

View your work.

govendor list

Look at what is using a package

govendor list -v fmt

Specify a specific version or revision to fetch

govendor fetchgolang.org/x/net/context@a4bbce9fcae005b22ae5443f6af064d80a6f5a55

govendor fetch golang.org/x/net/context@v1 # Get latest v1. . tag or branch.

govendor fetch golang.org/x/net/context@=v1 # Get the tag or branch named "v1".

Update a package to latest, given any prior version constraint

govendor fetch golang.org/x/net/context

Format your repository only

govendor fmt +local

Build everything in your repository only

govendor install +local

Test your repository only

govendor test +local

Sub-commands

    init     Create the "vendor" folder and the "vendor.json" file.

    list     List and filter existing dependencies and packages.

    add      Add packages from $GOPATH.

    update   Update packages from $GOPATH.

    remove   Remove packages from the vendor folder.

    status   Lists any packages missing, out-of-date, or modified locally.

    fetch    Add new or update vendor folder packages from remote repository.

    sync     Pull packages into vendor folder from remote repository with revisions

                 from vendor.json file.

    migrate  Move packages from a legacy tool to the vendor folder with metadata.

    get      Like "go get" but copies dependencies into a "vendor" folder.

    license  List discovered licenses for the given status or import paths.

    shell    Run a "shell" to make multiple sub-commands more efficient for large

                 projects.
gotool commands that are wrapped:
  `+<status>` package selection may be used with them
fmt, build, install, clean, test, vet, generate, tool
 

Status

Packages can be specified by their “status”.

    +local    (l) packages in your project

    +external (e) referenced packages in GOPATH but not in current project

    +vendor   (v) packages in the vendor folder

    +std      (s) packages in the standard library
+excluded (x) externalpackagesexplicitelyexcludedfromvendoring
+unused  (u) packagesin thevendorfolder, butunused
+missing  (m) referencedpackagesbutnot found
 
+program  (p) package is a mainpackage
 
+outside  +external +missing
+all      +allpackages
 

Status can be referenced by their initial letters.

  • +std same as +s
  • +external same as +ext same as +e
  • +excluded same as +exc same as +x

Status can be logically composed:

  • +local,program (local AND program) local packages that are also programs
  • +local +vendor (local OR vendor) local packages or vendor packages
  • +vendor,program +std ((vendor AND program) OR std) vendor packages that are also programs 
    or std library packages
  • +vendor,^program (vendor AND NOT program) vendor package that are not “main” packages.

Package specifier

The full package-spec is:

<path>[::<origin>][{/...|/^}][@[<version-spec>]]

Some examples:

  • github.com/kardianos/govendor specifies a single package and single folder.
  • github.com/kardianos/govendor/... specifies govendor and all referenced 
    packages under that path.
  • github.com/kardianos/govendor/^ specifies the govendor folder and all 
    sub-folders. Useful for resources or if you don’t want a partial repository.
  • github.com/kardianos/govendor/^::github.com/myself/govendor same as above 
    but fetch from user “myself”.
  • github.com/kardianos/govendor/...@abc12032 all referenced packages at 
    revision abc12032 .
  • github.com/kardianos/govendor/...@v1 same as above, but get the most recent 
    “v1” tag, such as “v1.4.3”.
  • github.com/kardianos/govendor/...@=v1 get the exact version “v1”.

Packages and Status

You may specify multiple package-specs and multiple status in a single command.

Commands that accept status and package-spec:

  • list
  • add
  • update
  • remove
  • fetch

You may pass arguments to govendor through stdin if the last argument is a “-“.

For example echo +vendor | govendor list - will list all vendor packages.

Ignoring build tags and excluding packages

Ignoring build tags is opt-out and is designed to be the opposite of the build

file directives which are opt-in when specified. Typically a developer will

want to support cross platform builds, but selectively opt out of tags, tests,

and architectures as desired.

To ignore additional tags edit the “vendor.json” file and add tag to the vendor

“ignore” file field. The field uses spaces to separate tags to ignore.

For example the following will ignore both test and appengine files.

{

    "ignore": "test appengine",

}

Similarly, some specific packages can be excluded from the vendoring process.

These packages will be listed as excluded ( x ), and will not be copied to the

“vendor” folder when running govendor add|fetch|update .

Any sub-package foo/bar of an excluded package foo is also excluded (but

package bar/foo is not). The import dependencies of excluded packages are not

listed, and thus not vendored.

To exclude packages, also use the “ignore” field of the “vendor.json” file.

Packages are identified by their name, they should contain a “/” character

(possibly at the end):

{

    "ignore": "test appengine foo/",

}

Go项目包管理工具 govendor

Go 1.5中(目前最新版本go1.5beta3)加入了一个experimental feature: vendor/。这个feature不是Go 1.5的正式功能,但却是Go Authors们在解决Go被外界诟病的包依赖管理的道路上的一次重要尝试。目前关于Go vendor机制的资料有限,主要的包括如下几个:

1、Russ Cox在Golang-dev group上的一个名 为"proposal: external packages" topic上的reply。
2、Go 1.5beta版发布后Russ Cox根据上面topic整理的一个doc
3、medium.com上一篇名为“Go 1.5 vendor/ experiment"的文章。

但由于Go 1.5稳定版还未发布(最新消息是2015.8月中旬发布),因此估计真正采用vendor的repo尚没有。但既然是Go官方解决方案,后续从 expreimental变成official的可能性就很大(Russ的初步计划:如果试验顺利,1.6版本默认 GO15VENDOREXPERIMENT="1";1.7中将去掉GO15VENDOREXPERIMENT环境变量)。因此对于Gophers们,搞 清楚vendor还是很必要的。本文就和大家一起来理解下vendor这个新feature。

一、vendor由来

Go第三方包依赖和管理的问题由来已久,民间知名的解决方案就有godep、 gb等。这次Go team在推出vendor前已经在Golang-dev group上做了长时间的调研,最终Russ Cox在Keith Rarick的proposal的基础上做了改良,形成了Go 1.5中的vendor。

Russ Cox基于前期调研的结果,给出了vendor机制的群众意见基础:
    – 不rewrite gopath
    – go tool来解决
    – go get兼容
    – 可reproduce building process

并给出了vendor机制的"4行"诠释:

If there is a source directory d/vendor, then, when compiling a source file within the subtree rooted at d, import "p" is interpreted as import "d/vendor/p" if that exists.

When there are multiple possible resolutions,the most specific (longest) path wins.

The short form must always be used: no import path can  contain “/vendor/” explicitly.

Import comments are ignored in vendored packages.

这四行诠释在group中引起了强烈的讨论,短小精悍的背后是理解上的不小差异。我们下面逐一举例理解。

二、vendor基本样例

Russ Cox诠释中的第一条是vendor机制的基础。粗犷的理解就是如果有如下这样的目录结构:

d/
   vendor/
          p/
           p.go
   mypkg/
          main.go

如果mypkg/main.go中有"import p",那么这个p就会被go工具解析为"d/vendor/p",而不是$GOPATH/src/p。

现在我们就来复现这个例子,我们在go15-vendor-examples/src/basic下建立如上目录结构(其中go15-vendor-examples为GOPATH路径):

$ls -R
d/

./d:
mypkg/    vendor/

./d/mypkg:
main.go

./d/vendor:
p/

./d/vendor/p:
p.go

其中main.go代码如下:

//main.go
package main

import "p"

func main() {
    p.P()
}

p.go代码如下:

//p.go
package p

import "fmt"

func P() {
    fmt.Println("P in d/vendor/p")
}

在未开启vendor时,我们编译d/mypkg/main.go会得到如下错误结果:

$ go build main.go
main.go:3:8: cannot find package "p" in any of:
    /Users/tony/.bin/go15beta3/src/p (from $GOROOT)
    /Users/tony/OpenSource/github.com/experiments/go15-vendor-examples/src/p (from $GOPATH)

错误原因很显然:go编译器无法找到package p,d/vendor下的p此时无效。

这时开启vendor:export GO15VENDOREXPERIMENT=1,我们再来编译执行一次:
$go run main.go
P in d/vendor/p

开启了vendor机制的go tool在d/vendor下找到了package p。

也就是说拥有了vendor后,你的project依赖的第三方包统统放在vendor/下就好了。这样go get时会将第三方包同时download下来,使得你的project无论被下载到那里都可以无需依赖目标环境而编译通过(reproduce the building process)。

三、嵌套vendor

那么问题来了!如果vendor中的第三方包中也包含了vendor目录,go tool是如何choose第三方包的呢?我们来看看下面目录结构(go15-vendor-examples/src/embeded):

d/
   vendor/
          p/
            p.go
          q/
            q.go
            vendor/
               p/
                 p.go
   mypkg/
          main.go

embeded目录下出现了嵌套vendor结构:main.go依赖的q包本身还有一个vendor目录,该vendor目录下有一个p包,这样我们就有了两个p包。到底go工具会选择哪个p包呢?显然为了验证一些结论,我们源文件也要变化一下:

d/vendor/p/p.go的代码不变。

//d/vendor/q/q.go
package q

import (
    "fmt"
    "p"
)

func Q() {
    fmt.Println("Q in d/vendor/q")
    p.P()
}

//d/vendor/q/vendor/p/p.go
package p

import "fmt"

func P() {
    fmt.Println("P in d/vendor/q/vendor/p")
}

//mypkg/main.go
package main

import (
    "p"
    "q"
)

func main() {
    p.P()
    fmt.Println("")
    q.Q()
}

目录和代码编排完毕,我们就来到了见证奇迹的时刻了!我们执行一下main.go:

$go run main.go
P in d/vendor/p

Q in d/vendor/q
P in d/vendor/q/vendor/p

可以看出main.go中最终引用的是d/vendor/p,而q.Q()中调用的p.P()则是d/vendor/q/vendor/p包的实现。go tool到底是如何在嵌套vendor情况下选择包的呢?我们回到Russ Cox关于vendor诠释内容的第二条:

   When there are multiple possible resolutions,the most specific (longest) path wins.

这句话很简略,但却引来的巨大争论。"longest path wins"让人迷惑不解。如果仅仅从字面含义来看,上面main.go的执行结果更应该是:

P in d/vendor/q/vendor/p

Q in d/vendor/q
P in d/vendor/q/vendor/p

d/vendor/q/vendor/p可比d/vendor/p路径更long,但go tool显然并未这么做。它到底是怎么做的呢?talk is cheap, show you the code。我们粗略翻看一下go tool的实现代码:

在$GOROOT/src/cmd/go/pkg.go中有一个方法vendoredImportPath,这个方法在go tool中广泛被使用

// vendoredImportPath returns the expansion of path when it appears in parent.
// If parent is x/y/z, then path might expand to x/y/z/vendor/path, x/y/vendor/path,
// x/vendor/path, vendor/path, or else stay x/y/z if none of those exist.
// vendoredImportPath returns the expanded path or, if no expansion is found, the original.
// If no expansion is found, vendoredImportPath also returns a list of vendor directories
// it searched along the way, to help prepare a useful error message should path turn
// out not to exist.
func vendoredImportPath(parent *Package, path string) (found string, searched []string)

这个方法的doc讲述的很清楚,这个方法返回所有可能的vendor path,以parentpath为x/y/z为例:

x/y/z作为parentpath输入后,返回的vendorpath包括:
   
x/y/z/vendor/path
x/y/vendor/path
x/vendor/path
vendor/path

这么说还不是很直观,我们结合我们的embeded vendor的例子来说明一下,为什么结果是像上面那样!go tool是如何resolve p包的!我们模仿go tool对main.go代码进行编译(此时vendor已经开启)。

根据go程序的package init顺序,go tool首先编译p包。如何找到p包呢?此时的编译对象是d/mypkg/main.go,于是乎parent = d/mypkg,经过vendordImportPath处理,可能的vendor路径为:

d/mypkg/vendor
d/vendor

但只有d/vendor/下存在p包,于是go tool将p包resolve为d/vendor/p,于是下面的p.P()就会输出:
P in d/vendor/p

接下来初始化q包。与p类似,go tool对main.go代码进行编译,此时的编译对象是d/mypkg/main.go,于是乎parent = d/mypkg,经过vendordImportPath处理,可能的vendor路径为:

d/mypkg/vendor
d/vendor

但只有d/vendor/下存在q包,于是乎go tool将q包resolve为d/vendor/q,由于q包自身还依赖p包,于是go tool继续对q中依赖的p包进行选择,此时go tool的编译对象变为了d/vendor/q/q.go,parent = d/vendor/q,于是经过vendordImportPath处理,可能的vendor路径为:

d/vendor/q/vendor
d/vendor/vendor
d/vendor

存在p包的路径包括:

d/vendor/q/vendor/p
d/vendor/p

此时按照Russ Cox的诠释2:choose longest,于是go tool选择了d/vendor/q/vendor/p,于是q.Q()中的p.P()输出的内容就是:
"
P in d/vendor/q/vendor/p"

如果目录结构足够复杂,这个resolve过程也是蛮繁琐的,但按照这个思路依然是可以分析出正确的包的。

另外vendoredImportPath传入的parent x/y/z并不是一个绝对路径,而是一个相对于$GOPATH/src的路径。

BTW,上述测试样例代码在这里可以下载到。

四、第三和第四条

最难理解的第二条已经pass了,剩下两条就比较好理解了。

The short form must always be used: no import path can  contain “/vendor/” explicitly.

这条就是说,你在源码中不用理会vendor这个路径的存在,该怎么import包就怎么import,不要出现import "d/vendor/p"的情况。vendor是由go tool隐式处理的。

Import comments are ignored in vendored packages.

go 1.4引入了canonical imports机制,如:

package pdf // import "rsc.io/pdf"

如果你引用的pdf不是来自rsc.io/pdf,那么编译器会报错。但由于vendor机制的存在,go tool不会校验vendor中package的import path是否与canonical import路径是否一致了。

五、问题

根据小节三中的分析,对于vendor中包的resolving过程类似是一个recursive(递归)过程。

main.go中的p使用d/vendor/p;而q.go中的p使用的是d/vendor/q/vendor/p,这样就会存在一个问题:一个工程中存 在着两个版本的p包,这也许不会带来问题,也许也会是问题的根源,但目前来看从go tool的视角来看似乎没有更好的办法。Russ Cox期望大家良好设计工程布局,作为lib的包不携带vendor更佳。

这样一个project内的所有vendor都集中在顶层vendor里面。就像下面这样:

d/
    vendor/   
            q/
            p/
            … …
    mypkg1
            main.go
    mypkg2
            main.go
    … …

另外Go vendor不支持第三方包的版本管理,没有类似godep的Godeps.json这样的存储包元信息的文件。不过目前已经有第三方的vendor specs放在了github上,之前Go team的Brad Fizpatrick也在Golang-dev上征集过类似的方案,不知未来vendor是否会支持。

六、vendor vs. internal

在golang-dev有人提到:有了vendor,internal似乎没用了。这显然是混淆了internal和vendor所要解决的问题。

internal故名思议:内部包,不是对所有源文件都可见的。vendor是存储和管理外部依赖包,更类似于external,里面的包都是copy自 外部的,工程内所有源文件均可import vendor中的包。另外internal在1.4版本中已经加入到go核心,是不可能轻易去除的,虽然到目前为止我们还没能亲自体会到internal 包的作用。

在《Go 1.5中值得关注的几个变化》一文中我提到过go 1.5 beta1似乎“不支持”internal,beta3发布后,我又试了试看beta3是否支持internal包。

结果是beta3中,build依旧不报错。但go list -json会提示错误:
"DepsErrors": [
        {
            "ImportStack": [
                "otherpkg",
                "mypkg/internal/foo"
            ],
            "Pos": "",
            "Err": "use of internal package not allowed"
        }
    ]

难道真的要到最终go 1.5版本才会让internal包发挥作用?


长期以来,golang 对外部依赖都没有很好的管理方式,只能从 $GOPATH 下查找依赖。这就造成不同用户在安装同一个项目适合可能从外部获取到不同的依赖库版本,同时当无法联网时,无法编译依赖缺失的项目。

自 1.5 版本开始引入 govendor 工具,该工具将项目依赖的外部包放到项目下的 vendor 目录下(对比 nodejs 的 node_modules 目录),并通过 vendor.json 文件来记录依赖包的版本,方便用户使用相对稳定的依赖。

对于 govendor 来说,主要存在三种位置的包:项目自身的包组织为本地(local)包;传统的存放在 $GOPATH 下的依赖包为外部(external)依赖包;被 govendor 管理的放在 vendor 目录下的依赖包则为 vendor 包。

具体来看,这些包可能的类型如下:

状态 缩写状态 含义
+local l 本地包,即项目自身的包组织
+external e 外部包,即被 $GOPATH 管理,但不在 vendor 目录下
+vendor v 已被 govendor 管理,即在 vendor 目录下
+std s 标准库中的包
+unused u 未使用的包,即包在 vendor 目录下,但项目并没有用到
+missing m 代码引用了依赖包,但该包并没有找到
+program p 主程序包,意味着可以编译为执行文件
+outside   外部包和缺失的包
+all   所有的包

常见的命令如下,格式为 govendor COMMAND

通过指定包类型,可以过滤仅对指定包进行操作。

命令 功能
init 初始化 vendor 目录
list 列出所有的依赖包
add 添加包到 vendor 目录,如 govendor add +external 添加所有外部包
add PKG_PATH 添加指定的依赖包到 vendor 目录
update 从 $GOPATH 更新依赖包到 vendor 目录
remove 从 vendor 管理中删除依赖
status 列出所有缺失、过期和修改过的包
fetch 添加或更新包到本地 vendor 目录
sync 本地存在 vendor.json 时候拉去依赖包,匹配所记录的版本
get 类似 go get 目录,拉取依赖包到 vendor 目录


猜你喜欢

转载自blog.csdn.net/fanpengfei0/article/details/56680139
今日推荐