vendor

boscoyoung · 2016-11-20 08:36:21 · 10204 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2016-11-20 08:36:21 的主题,其中的信息可能已经有所发展或是发生改变。

引言

Go Vendoring是一种对GOPATH的扩展,其意义在于:让原生的工具链无缝支持第三方库的版本管理,如go build/run/test.

在Go1.5之前,多个项目(ProjA,ProjB)如果想要引用某个第三方库的不同版本,可以在编译时修改GOPATH,将对应的第三方库加入GOPATH中,这样一来势必需要在原生工具链上层有一层脚本或Makefile抽象.

Go Vendoring解决了这个问题,实现了GOPATH的扩展:将项目ProjA依赖的外部库代码放到具体位置的时候,直接用原生的Go Command是可以找到、识别这些代码文件(就好像修改了GOPATH一样).

Vendoring是Go1.5中引入的实验特性,Go1.6成为正式特性,Go1.7中成为标准特性.

Go1.5和Go1.6中用GO15VENDOREXPERIMENT控制vendor的生效与否(在Go1.5中,vendoring默认关闭,但Go1.6中默认打开), 在Go1.7去掉GO15VENDOREXPERIMENT开关,成为标准特性:

export GO15VENDOREXPERIMENT=0 # 关闭vendoring功能
export GO15VENDOREXPERIMENT=1 # 打开vendoring功能

本文介绍Vendoring的用法时,用到了以下例子:

▾ src/
  ▾ p1/
    ▾ p2/
      ▾ p3/
        ▾ p7/
          ▾ vendor/p8/
              p8.go
            p7.go
        ▾ vendor/p9/
            p9.go
          p3.go
      ▾ p5/p6/vendor/p13/
          p13.go
      ▾ vendor/p10/
          p10.go
    ▾ vendor/p11/
        p11.go
  ▾ p100/
      main.go
  ▾ vendor/p12/
      p12.go
    main.go

源码笔记

这里所有的源码都是基于go1.6.2的,不同版本的源码可能会有差别

核心逻辑入口位于 src/cmd/go/pkg.go 中:

# src/cmd/go/pkg.go:L317
317 func loadImport(path, srcDir string, parent *Package, stk *importStack, importPos []token.Position, mode int) *Package {
// 初始化上下文
329     if isLocal {
330         importPath = dirToImportPath(filepath.Join(srcDir, path))
331     } else if mode&useVendor != 0 {
// 如果开启了vendor,就会构造vendor路径(如果目录存在就加入列表,不存在就舍弃)
336         path = vendoredImportPath(parent, path)
337         importPath = path
338     }
339
// 如果package/path cache了对应的的import path,直接返回vendored path,然后进行一些校验
// 校验分为internal/vendor校验,主要就是处理一些不合法的import
// 递归遍历package import
377     p.load(stk, bp, err)
// 校验import internal package的合法性
// 校验import vendor package的合法性

实际构造和探测vendor路径的代码是从 src/cmd/go/pkg.go:L326 调用的:

// src/cmd/go/pkg.goL414
// 336         path = vendoredImportPath(parent, path)
 414 func vendoredImportPath(parent *Package, path string) (found string) {
 415     if parent == nil || parent.Root == "" || !go15VendorExperiment {
 416         return path
 417     }
 418
 419     dir := filepath.Clean(parent.Dir)
 420     root := filepath.Join(parent.Root, "src")
 421     if !hasFilePathPrefix(dir, root) {
 422         // Look for symlinks before reporting error.
 423         dir = expandPath(dir)
 424         root = expandPath(root)
 425     }
 426     if !hasFilePathPrefix(dir, root) || len(dir) <= len(root) || dir[len(root)] != filepath.Separator {
 427         fatalf("invalid vendoredImportPath: dir=%q root=%q separator=%q", dir, root, string(filepath.Separator))
 428     }
 429
 430     vpath := "vendor/" + path
 431     for i := len(dir); i >= len(root); i-- {
 432         if i < len(dir) && dir[i] != filepath.Separator {
 433             continue
 434         }
 435         // Note: checking for the vendor directory before checking
 436         // for the vendor/path directory helps us hit the
 437         // isDir cache more often. It also helps us prepare a more useful
 438         // list of places we looked, to report when an import is not found.
 439         if !isDir(filepath.Join(dir[:i], "vendor")) {
 440             continue
 441         }
 442         targ := filepath.Join(dir[:i], vpath)
 443         if isDir(targ) && hasGoFiles(targ) {
 444             importPath := parent.ImportPath
 445             if importPath == "command-line-arguments" {
 446                 // If parent.ImportPath is 'command-line-arguments'.
 447                 // set to relative directory to root (also chopped root directory)
 448                 importPath = dir[len(root)+1:]
 449             }
 450             // We started with parent's dir c:\gopath\src\foo\bar\baz\quux\xyzzy.
 451             // We know the import path for parent's dir.
 452             // We chopped off some number of path elements and
 453             // added vendor\path to produce c:\gopath\src\foo\bar\baz\vendor\path.
 454             // Now we want to know the import path for that directory.
 455             // Construct it by chopping the same number of path elements
 456             // (actually the same number of bytes) from parent's import path
 457             // and then append /vendor/path.
 458             chopped := len(dir) - i
 459             if chopped == len(importPath)+1 {
 460                 // We walked up from c:\gopath\src\foo\bar
 461                 // and found c:\gopath\src\vendor\path.
 462                 // We chopped \foo\bar (length 8) but the import path is "foo/bar" (length 7).
 463                 // Use "vendor/path" without any prefix.
 464                 return vpath
 465             }
 466             return importPath[:len(importPath)-chopped] + "/" + vpath
 467         }
 468     }
 469     return path
 470 }

import $package的遍历都是以parent为上下文的,比如有如下项目结构:

// $proj_root加入GOPATH
$proj_root
  |- src
    |- p1
      |- p2
        |- p3
          |- p3.go

其中 $proj_root/src/p1/p2/p3/p3.go 引入了 p4

# $proj_root/src/p1/p2/p3/p3.go
package p3

import "p4"

// 其他实际逻辑

上面 vendoredImportPath 的核心算法如下:

path := 依次扫描以下路径:
  - $proj_root/src/p1/p2/p3/vendor/p4 => 相对路径:p1/p2/p3/vendor/p4
  - $proj_root/src/p1/p2/vendor/p4    => 相对路径:p1/p2/vendor/p4
  - $proj_root/src/p1/vendor/p4       => 相对路径:p1/vendor/p4
  - $proj_root/src/vendor/p4          => 相对路径:vendor/p4

if path 是路径? && path有*.go文件
  返回该路径对应的GOPATH路径
end

可见vendor依赖还是需要放到$GOPATH下的,因为算法返回的实际上还是一个相对路径.

src/cmd/go/pkg.go#L415 中有一个判断逻辑,所有parent.Root为空的package都不会探测vendored package,所以$GOPATH/src下直接写的main文件(package)是不支持vendoring的.

415     if parent == nil || parent.Root == "" || !go15VendorExperiment {

上面的算法获取到所有合法的 vendored path,在 disallowVendor 还检查了import的时候没有直接 import vendor/p4等,这些在开启了vendor后都是不合法的,编译会报错:

 588 func disallowVendor(srcDir, path string, p *Package, stk *importStack) *Package {
 589     if !go15VendorExperiment {
 590         return p
 591     }
 592
 593     // The stack includes p.ImportPath.
 594     // If that's the only thing on the stack, we started
 595     // with a name given on the command line, not an
 596     // import. Anything listed on the command line is fine.
 597     if len(*stk) == 1 {
 598         return p
 599     }
 600
 601     if perr := disallowVendorVisibility(srcDir, p, stk); perr != p {
 602         return perr
 603     }
 604
 605     // Paths like x/vendor/y must be imported as y, never as x/vendor/y.
 606     if i, ok := findVendor(path); ok {
 607         perr := *p
 608         perr.Error = &PackageError{
 609             ImportStack: stk.copy(),
 610             Err:         "must be imported as " + path[i+len("vendor/"):],
 611         }
 612         perr.Incomplete = true
 613         return &perr
 614     }
 615
 616     return p
 617 }

前面递归查找的时候并不检查vendor的合法性,所以可能出现 import p4 被映射到了 import p5/vendor/p4 的情况,这种情况会报错 "use of vendored package not allowed".

虽然说查找的时候只往上查,但由于Go中的package查找是一个递归算法,其中应用了cache map[imported path] => vendored path,有可能当前获取到的vendored path是不合法的,所以存在这一层校验.

// 如果在p3.go里面import p4会报错,因为visibility校验失败了
$proj_root
  |- src
    |- p1
      |- p2
        |- p3
          |- p3.go
        |- p5
          |- vendor
            |- p4
              |- p4.go

vendor import是通过 findVendor判断的

 678 func findVendor(path string) (index int, ok bool) {
 679     // Two cases, depending on internal at start of string or not.
 680     // The order matters: we must return the index of the final element,
 681     // because the final one is where the effective import path starts.
 682     switch {
 683     case strings.Contains(path, "/vendor/"):
 684         return strings.LastIndex(path, "/vendor/") + 1, true
 685     case strings.HasPrefix(path, "vendor/"):
 686         return 0, true
 687     }
 688     return 0, false
 689 }

可见诸如 import vendor/p1 import p1/vendor/p2 都是不合法的.

用法分析

Vendoring提供了GOPATH的扩展:将项目A依赖的外部库代码放到具体位置的时候,直接用原生的Go Command是可以找到、识别这些代码文件(好像修改了GOPATH一样). 外部依赖的管理,如clone具体的第三方依赖代码并checkout具体版本, 需要通过Go Tools以外的工具实现.

Go Vendor比较常用的规则:

1) Vendored Package必须在GOROOTGOPATH

因为Vendored Package都是以GOROOTGOPATH为基础扫描的,使用local import时是无法使用Vendor机制的.

2) Vendored Package优先

这里的优先应该有两层含义:

  • Vendored Package的扫描优先级,即src/p1/p2/p3/p3.goimport "p12"的时候,依次扫描了src/p1/p2/p3/vendor/p12, src/p1/p2/vendor/p12, src/p1/vendor/p12, src/vendor/p12,返回第一个存在且下面有Go源码文件的Package
  • Vendored PackageGOROOTGOPATH下普通的package的优先级,会优先使用Vendor Package

3) 直接放在GOROOT|GOPATH/src下的main package不支持Vendoring

这个可能是Go里面的小Bug,似乎这方面的需求也不明显. 前面例子里的 src/main.go,尽管src/vendor/p12存在,所以下面的main.go直接import "p12"会报package找不到:

  // src/main.go
  1 package main
  2
  3 import (
  4     "p1/p2/p3"
  5     "p1/p2/p3/p7"
  6     "p12"
  7 )
  8
  9 func main() {
 10     p3.Hello3()
 11     p7.Hello7()
 12     p12.Hello12()
 13 }

 $ go run main.go

 main.go:6:2: cannot find package "p12" in any of:
        /usr/local/go/src/p12 (from $GOROOT)
        /Users/yangyuqian/demo/src/p12 (from $GOPATH)

解决方案是把这样的入口文件放到具体的子目录下面去,就可以正常编译了.

// src/p100/main.go

4) Vendored Package只能往上找

Vendored Package的查找是一个动态匹配的过程,前面的例子中:

// src/p1/p2/p3/p3.go
  1 package p3
  2
  3 import (
  4     "p12"
  5 )
  6
  7 func Hello3() {
  8     println("Hello, P3")
  9     p12.Hello12()
 10 }

这里 import 了 src/vendor/p12,这是最终匹配到的vendor路径,实际上这里经历了以下的匹配:

  • src/p1/p2/p3/vendor/p12
  • src/p1/p2/vendor/p12
  • src/p1/vendor/p12
  • src/vendor/p12

可见前几个都没有匹配中,所以返回第一个命中的路径 src/vendor/p12

只往上找,意味着 src/p1/p2/p5/p6/vendor/p13src/p1/p2/p3/p7/vendor/p8src/p1/p2/p3/p3.go是不可见的.


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

10204 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传