标签 Package 下的文章

定制Go Package的Go Get导入路径

近期Go team的组员Jaana B. Dogan,网名:rakyll开源了一个小工具:Go Vanity URLs。这个小工具可以帮助你快速为你的Go package定制Go get的导入路径(同样也是package被使用时的import路径)。

说到go package的go get导入路径,我们最常见和常使用的domain name就是github.com了,比如:beego包的go get导入路径就是 go get github.com/astaxie/beego。我们还经常看到一些包,它们的导入路径很特殊,比如:go get golang.org/x/net、go get gopkg.in/yaml.v2等(虽然net、yaml这些包实际的repo也是存在于github.com上的),这些就是定制化的package import path,它们有诸多好处:

  • 可以为package设置canonical import path ,即权威导入路径

    这是在Go 1.4版本中加入的概念。Go package多托管在几个知名的代码管理网站,比如:github.com、bitbucket.org等,这样默认情况下package的import path就是github.com/xxx/package、bitbucket.org/xxx/package等。一旦某个网站关门大吉了,那package代码势必要迁移到其他站点,这样package的import path就要发生改变,这会给package的用户造成诸多不便,比如之前的code.google.com关闭就给广大的gopher带来了很大的“伤害”。canonical import path就可以解决这个问题。package的用户只需要使用package的canonical import path,这样无论package的实际托管网站在哪,对package的用户都不会带来影响。

  • 便于组织和个人对package的管理

    组织和个人可以将其分散托管在不同代码管理网站的package统一聚合到组织的官网名下或个人的域名下,比如:golang.org/x/net、gopkg.in/xxx等。

  • package的import路径可以更短、更简洁

    有些时候,github.com上的go package的import path很长、很深,并不便于查找和书写,通过定制化import path,我们可以使用更短、更简洁的域名来代替github.com仓库下的多级路径。

不过rakyll提供的govanityurls仅能运行于Google的app engine上,这对于国内的Gopher们来说是十分不便的,甚至是不可用的,于是这里fork了rakyll的repo,并做了些许修改,让govanityurls可以运行于普通的vps主机上。

一、govanityurls原理

govanityurls的原理十分简单,它本身就好比一个“导航”服务器。当go get将请求发送给govanityurls时,govanityurls将请求中的repo的真实地址返回给go get,后续go get再从真实的repo地址获取package数据。

img{512x368}

可以看出go get第一步是尝试获取自定义路径的包的真实地址,govanityurls将返回一个类似如下内容的http应答(针对go get tonybai.com/gowechat请求):

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="go-import" content="tonybai.com/gowechat git https://github.com/bigwhite/gowechat">
<meta name="go-source" content="tonybai.com/gowechat ">
<meta http-equiv="refresh" content="0; url=https://godoc.org/tonybai.com/gowechat">
</head>
<body>
Nothing to see here; <a href="https://godoc.org/tonybai.com/gowechat">see the package on godoc</a>.
</body>
</html>

二、使用govanityurls

关于govanityurls的使用,可以参考其README.md,这里以一个demo来作为govanityurls的使用说明。

1、安装govanityurls

安装方法:

$go get github.com/bigwhite/govanityurls

$govanityurls
govanityurls is a service that allows you to set custom import paths for your go packages

Usage:
     govanityurls -host [HOST_NAME]

  -host string
        custom domain name, e.g. tonybai.com

和rakyll提供的govanityurls不同的是,这里的govanityurls需要外部传入一个host参数(比如:tonybai.com),而在原版中这个host是由google app engine的API提供的。

2、配置vanity.yaml

vanity.yaml中配置了host下的自定义包的路径以及其真实的repo地址:

/gowechat:
        repo: https://github.com/bigwhite/gowechat

上面这个配置中,我们实际上为gowechat这个package定义了tonybai.com/gowechat这个go get路径,其真实的repo存放在github.com/bigwhite/gowechat。当然这个vanity.yaml可以配置N个自定义包路径,也可以定义多级路径,比如:

/gowechat:
        repo: https://github.com/bigwhite/gowechat

/x/experiments:
        repo: https://github.com/bigwhite/experiments

3、配置反向代理

govanityurls默认监听的是8080端口,这主要是考虑到我们通常会使用主域名定制路径,而在主域名下面一般情况下都会有其他一些服务,比如:主页、博客等。通常我们都会用一个反向代理软件做路由分发。我们针对gowechat这个repo定义了一条nginx location规则:

// /etc/nginx/conf.d/default.conf
server {
        listen 80;
        listen 443 ssl;
        server_name tonybai.com;

        ssl_certificate           /etc/nginx/cert.crt;
        ssl_certificate_key       /etc/nginx/cert.key;
        ssl on;

        location /gowechat {
                proxy_pass http://10.11.36.23:8080;
                proxy_redirect off;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
        }
}

这里为了方便,我既在80端口提供http服务,也在443端口提供了https服务。这里的10.11.36.23就是我真正部署govanityurls的host(一台thinkcenter PC)。/etc/nginx/cert.key和/etc/nginx/cert.crt可以通过下面命令生成:

sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/cert.key -out /etc/nginx/cert.crt

CN填tonybai.com

注意:修改两个文件的owner权限,将其owner改为nginx worker process的user,我这里是www-data(chown www-data:www-data /etc/nginx/cert.*)。

4、测试govanityurls

我在我的mac上修改了一下/etc/hosts,添加一条路由:

10.11.36.23 tonybai.com

我们来go get tonybai.com/gowechat:

$go get -v -insecure tonybai.com/gowechat
Fetching https://tonybai.com/gowechat?go-get=1
https fetch failed: Get https://tonybai.com/gowechat?go-get=1: EOF
Fetching http://tonybai.com/gowechat?go-get=1
Parsing meta tags from http://tonybai.com/gowechat?go-get=1 (status code 200)
get "tonybai.com/gowechat": found meta tag main.metaImport{Prefix:"tonybai.com/gowechat", VCS:"git", RepoRoot:"https://github.com/bigwhite/gowechat"} at http://tonybai.com/gowechat?go-get=1
tonybai.com/gowechat (download)
package tonybai.com/gowechat: no buildable Go source files in /Users/tony/Test/GoToolsProjects/src/tonybai.com/gowechat

$ls /Users/tony/Test/GoToolsProjects/src/tonybai.com/gowechat
LICENSE        README.md    mp/        pb/        qy/

我们可以看到tonybai.com/gowechat被成功get到本地,并且import path为tonybai.com/gowechat,其他包可以按照这个定制的gowechat的导入路径import gowechat package了。

上面例子中,我们给go get传入了一个-insecure的参数,这样go get就会通过http协议去访问tonybai.com/gowechat了。我们试试去掉-insecure,不过再次执行前需先将本地的tonybai.com/gowechat包删除掉。

$go get -v tonybai.com/gowechat
Fetching https://tonybai.com/gowechat?go-get=1
https fetch failed: Get https://tonybai.com/gowechat?go-get=1: x509: certificate signed by unknown authority
package tonybai.com/gowechat: unrecognized import path "tonybai.com/gowechat" (https fetch: Get https://tonybai.com/gowechat?go-get=1: x509: certificate signed by unknown authority)

虽然我已经关掉了git的http.sslVerify,但go get的执行过程还是检查了server端证书是未知CA签署的并报错,原来这块的verify是go get自己做的。关于httpskey和证书(.crt)的相关知识,我在《Go和HTTPS》一文中已经做过说明,不是很熟悉的童鞋可以移步那篇文章。

我们来创建CA、创建server端的key(cert.key),并用创建的CA来签署server.crt:

$ openssl genrsa -out rootCA.key 2048
$ openssl req -x509 -new -nodes -key rootCA.key -subj "/CN=*.tonybai.com" -days 5000 -out rootCA.pem
$ openssl genrsa -out cert.key 2048
$ openssl req -new -key cert.key -subj "/CN=tonybai.com" -out cert.csr
$ openssl x509 -req -in cert.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out cert.crt -days 5000

# ls
cert.crt  cert.csr  cert.key  rootCA.key  rootCA.pem  rootCA.srl

我们将cert.crt和cert.key拷贝到ubuntu的/etc/nginx目录下,重启nginx,让其加载新的cert.crt和cert.key。然后将rootCA.pem拷贝到/etc/ssl/cert目录下,这个目录是ubuntu下存放CA公钥证书的标准路径。在测试go get前,我们先用curl测试一下:

# curl https://tonybai.com/gowechat
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="go-import" content="tonybai.com/gowechat git https://github.com/bigwhite/gowechat">
<meta name="go-source" content="tonybai.com/gowechat ">
<meta http-equiv="refresh" content="0; url=https://godoc.org/tonybai.com/gowechat">
</head>
<body>
Nothing to see here; <a href="https://godoc.org/tonybai.com/gowechat">see the package on godoc</a>.
</body>
</html>

curl测试通过!

我们再来看看go get:

# go get tonybai.com/gowechat
package tonybai.com/gowechat: unrecognized import path "tonybai.com/gowechat" (https fetch: Get https://tonybai.com/gowechat?go-get=1: x509: certificate signed by unknown authority)

问题依旧!难道go get无法从/etc/ssl/cert中选取适当的ca证书来做server端的cert.crt的验证么?就着这个问题我在go官方发现了一个类似的issue: #18519 。从中得知,go get仅仅会在不同平台下参考以下几个certificate files:

$GOROOT/src/crypto/x509/root_linux.go

package x509

// Possible certificate files; stop after finding one.
var certFiles = []string{
    "/etc/ssl/certs/ca-certificates.crt",                // Debian/Ubuntu/Gentoo etc.
    "/etc/pki/tls/certs/ca-bundle.crt",                  // Fedora/RHEL 6
    "/etc/ssl/ca-bundle.pem",                            // OpenSUSE
    "/etc/pki/tls/cacert.pem",                           // OpenELEC
    "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7
}

在ubuntu上,/etc/ssl/certs/ca-certificates.crt是其参考的数字证书。因此要想go get成功,我们需要将我们rootCA.pem加入到/etc/ssl/certs/ca-certificates.crt中去,最简单的方法就是:

$ cat rootCA.pem >> /etc/ssl/certs/ca-certificates.crt

当然,ubuntu也提供了管理根证书的命令update-ca-certificates,可以看其manual学学如何更新/etc/ssl/certs/ca-certificates.crt,这里就不赘述了。

更新后,我们再来go get:

# go get -v tonybai.com/gowechat
Fetching https://tonybai.com/gowechat?go-get=1
Parsing meta tags from https://tonybai.com/gowechat?go-get=1 (status code 200)
get "tonybai.com/gowechat": found meta tag main.metaImport{Prefix:"tonybai.com/gowechat", VCS:"git", RepoRoot:"https://github.com/bigwhite/gowechat"} at https://tonybai.com/gowechat?go-get=1
tonybai.com/gowechat (download)
package tonybai.com/gowechat: no buildable Go source files in /root/go/src/tonybai.com/gowechat

go get成功!

三、小结

  • 使用govanityurls可以十分方便的为你的go package定制go get的导入路径;
  • 一般使用nginx等反向代理放置在govanityurls前端,便于同域名下其他服务的开展;
  • go get默认采用https访问,自签署的ca和server端的证书问题要处理好。如果有条件的话,还是用用letsencrypt等提供的免费证书吧。

微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

初窥dep

Go语言程序组织和构建的基本单元是Package,但Go语言官方却没有提供一款“像样的”Package Management Tool(包管理工具)。随着Go语言在全球范围内应用的愈加广泛,缺少官方包管理工具这一问题变得日益突出。

2016年GopherCon大会后,在Go官方的组织下,一个旨在改善Go包管理的commitee成立了,共同应对Go在package management上遇到的各种问题。经过各种脑洞和讨论后,该commitee在若干月后发布了“Package Management Proposal”,并启动了最有可能被接纳为官方包管理工具的项目dep的设计和开发。2017年年初,dep项目正式对外开放。截至目前,dep发布了v0.1.0版本,并处于alpha测试阶段。

可以说,dep的进展还是蛮快的。按照dep官方说法,dep目前的manifest和lock文件格式已经stable,并保证向后兼容。同时,dep实现了“自举”,即dep使用自己作为自己的包管理工具。由于dep的“特殊身份”,虽然dep离成熟尚远,但dep的进展也吸引了诸多gopher的目光,很多组织已经开始将package management tool迁移为dep,为dep进行早期测试。

这里,我也打算“尝尝鲜”,在本篇文章中和大家一起窥探和试用一下dep。

一、Go包管理的演进历史

1、go get

在管窥dep之前,我们先来简单看看Go语言包管理的演进历史。首当其冲的就是go get。

Go语言新手在初次接触Go语言时会感觉到Go语言的package获取真的是很方便:只需一行go get xxx,github.com上的大量go package就可以随你取用。 但随着对Go语言使用的深入,人们会发现go get给我们带来方便的同时,也带来了不少的麻烦。go get本质上是githg等这些vcs工具的高级wrapper。对于使用git的go package来说,go get的实质就是将package git clone到本地的特定目录下($GOPATH/src),同时go get可以自动解析包的依赖,并自动下载相关依赖包。

go get机制的设计很大程度上源于Google公司内部的单一root的代码仓库的开发模式,并且似乎google内部各个project/repository的master分支上的代码都是被认为stable的,因此go get仅仅支持获取master branch上的latest代码,没有指定version、branch或revision的能力。而在Google公司以外的世界里,这样的做法会给gopher带来不便:依赖的第三方包总是在变。一旦第三方包提交了无法正常build或接口不兼容的代码,依赖方立即就会受到影响。

而gopher们又恰恰希望自己项目所依赖的第三方包能受到自己的控制,而不是随意变化。这样,godepgbglide等一批第三方包管理工具出现了。

以应用最为广泛的godep为例。为了能让第三方依赖包“稳定下来”,实现项目的reproduceble build,godep将项目当前依赖包的版本信息记录在Godeps/Godeps.json中,并将依赖包的相关版本存放在Godeps/_workspace中。在编译时(godep go build)godep通过临时修改GOPATH环境变量的方法让go编译器使用缓存在Godeps/_workspace下的项目依赖的特定版本的第三方包,这样保证了项目不再受制于依赖的第三方包的master branch上的latest代码的变动了。

不过,godep的“版本管理”本质上是通过缓存第三方库的某个revision的快照实现的,这种方式依然让人感觉难于管理。同时,通过对GOPATH的“偷梁换柱”的方式实现使用Godeps/_workspace中的第三方库的快照进行编译也无法兼容Go原生编译器,必须使用godep go xxx来进行。

为此,Go进一步引入vendor机制减少gopher在包管理问题上的心智负担

2、vendor机制

Go team也一直在关注Go语言包依赖的问题,尤其是在Go 1.5实现自举的情况下,官方同样在1.5版本中推出了vendor机制。vendor机制是Russ CoxGo 1.5发布前期以一个experiment feature身份紧急加入到go中的(go 1.6脱离experiment身份)。vendor标准化了项目依赖的第三方库的存放位置(不再需要Godeps/_workspace了),同时也无需对GOPATH环境变量进行“偷梁换柱”了,go compiler原生优先感知和使用vendor下缓存的第三方包。

不过即便有了vendor的支持,vendor内第三方依赖包的代码的管理依旧是不规范的,要么是手动的,要么是借助godep这样的第三方包管理工具。目前自举后的Go代码本身也引入了vendor,不过go项目自身对vendor中代码的管理方式也是手动更新,Go自身并未使用任何第三方的包管理工具。

题外话:作为一门语言的标准库,应该是使用这门语言的开发者所使用的所有lib依赖的根依赖。但在go中,go标准库居然还要依赖golang.org/x/目录下的包,既然能被std lib依赖,那么说明其已经成熟,那为何不把x内的stable的库挪到std lib中呢?这点着实让人有些不解。

~/.bin/go18/src/vendor/golang_org/x]$ls
crypto/    net/    text/

从Go官方角度出发,官方go包依赖的解决方案的下一步就应该是解决对vendor下的第三方包如何进行管理的问题:依赖包的分析、记录和获取等,进而实现项目的reproducible build。dep就是用来做这事儿的。

二、dep简介

go package management commitee的牵头人物是微服务框架go-kit作者Peter Bourgon,但当前主导dep开发的是sam boyer,sam也是dep底层包依赖分析引擎-gps的作者。

和其他一些第三方Go包管理工具有所不同,dep在进行active dev前是经过commitee深思熟虑的,包括:featuresuser story等都在事前做了初步设计。如果你拜读这些文档,你可能会觉得解决包依赖问题,还是蛮复杂的。不过,对于这些工具的使用者来说,我们面对的是一些十分简化的交互接口。

1、安装dep

dep是标准的go cli程序,执行一条命令即完成安装:

# go get -u github.com/golang/dep/cmd/dep

# dep help
dep is a tool for managing dependencies for Go projects

Usage: dep <command>

Commands:

  init    Initialize a new project with manifest and lock files
  status  Report the status of the project's dependencies
  ensure  Ensure a dependency is safely vendored in the project
  prune   Prune the vendor tree of unused packages

Examples:
  dep init                          set up a new project
  dep ensure                        install the project's dependencies
  dep ensure -update                update the locked versions of all dependencies
  dep ensure github.com/pkg/errors  add a dependency to the project

Use "dep help [command]" for more information about a command.

在我的测试环境中,go的版本为1.8;dep的版本为commit d31c621c3381b9bebc7c10b1ac7849a96c21f2c3。

注意:由于dep还在active dev过程中且处于alpha测试阶段,因此本文中执行的dep命令、命令行为以及输出结果在后续dep版本中很可能会有变动,甚至是很大变动。

2、dep一般工作流

安装好dep后,我们就来看看使用dep的一般工作流。我们首先准备一个demo程序:

//depdemo/main.go
package main

import (
    "net/http"

    "go.uber.org/zap"

    "github.com/beego/mux"
)

func main() {
    logger, _ := zap.NewProduction()
    defer logger.Sync()
    sugar := logger.Sugar()

    mx := mux.New()
    mx.Handler("GET", "/", http.FileServer(http.Dir(".")))
    sugar.Fatal(http.ListenAndServe("127.0.0.1:8080", mx))
}

a) dep init

如果一个项目要使用dep进行包管理,那么首先需要在这个项目的根下执行dep init。在这里,我们对depdemo进行dep改造。

在depdemo目录下,执行dep init:

# dep init -v
Searching GOPATH for projects...
  Using master as constraint for direct dep github.com/beego/mux
  Locking in master (626af65) for direct dep github.com/beego/mux
Following dependencies were not found in GOPATH. Dep will use the most recent versions of these projects.
  go.uber.org/zap
Root project is "github.com/bigwhite/experiments/depdemo"
 1 transitively valid internal packages
 2 external packages imported from 2 projects
(0)   ✓ select (root)
(1)    ? attempt github.com/beego/mux with 1 pkgs; at least 1 versions to try
(1)        try github.com/beego/mux@master
(1)    ✓ select github.com/beego/mux@master w/1 pkgs
(2)    ? attempt go.uber.org/zap with 1 pkgs; 12 versions to try
(2)        try go.uber.org/zap@v1.4.0
(2)    ✓ select go.uber.org/zap@v1.4.0 w/7 pkgs
(3)    ? attempt go.uber.org/atomic with 1 pkgs; 6 versions to try
(3)        try go.uber.org/atomic@v1.2.0
(3)    ✓ select go.uber.org/atomic@v1.2.0 w/1 pkgs
  ✓ found solution with 9 packages from 3 projects

Solver wall times by segment:
     b-source-exists: 1.090607387s
  b-deduce-proj-root: 288.126482ms
         b-list-pkgs: 131.059753ms
              b-gmal: 114.716587ms
         select-atom:    337.787µs
             satisfy:    298.743µs
         select-root:    292.889µs
            new-atom:    257.256µs
     b-list-versions:     42.408µs
               other:     22.307µs

  TOTAL: 1.625761599s

当前阶段,dep init命令的执行效率的确不高,因此需要你耐心的等待一会儿。如果你的project依赖的外部包很多,那么等待的时间可能会很长。并且由于dep会下载依赖包,对于国内的朋友来说,一旦下载qiang外的包,那么dep可能会“阻塞”在那里!

dep init大致会做这么几件事:

  • 利用gps分析当前代码包中的包依赖关系;
  • 将分析出的项目包的直接依赖(即main.go显式import的第三方包,direct dependency)约束(constraint)写入项目根目录下的Gopkg.toml文件中;
  • 将项目依赖的所有第三方包(包括直接依赖和传递依赖transitive dependency)在满足Gopkg.toml中约束范围内的最新version/branch/revision信息写入Gopkg.lock文件中;
  • 创建root vendor目录,并且以Gopkg.lock为输入,将其中的包(精确checkout 到revision)下载到项目root vendor下面。

执行完dep init后,dep会在当前目录下生成若干文件:

├── Gopkg.lock
├── Gopkg.toml
├── main.go
└── vendor/

我们逐一来看一下:

Gopkg.toml:

[[constraint]]
  branch = "master"
  name = "github.com/beego/mux"

[[constraint]]
  name = "go.uber.org/zap"
  version = "1.4.0"

Gopkg.toml记录了depdemo/main.go的两个direct dependency:mux和zap。通过gps的分析(可以参见上面init执行时输出的详细分析过程日志),dep确定的依赖版本约束为:mux的master分支、zap的1.4.0 version。

生成的Gopkg.lock中则记录了depdemo/main.go在上述约束下的所有依赖的可用的最新版本:

Gopkg.lock:

[[projects]]
  branch = "master"
  name = "github.com/beego/mux"
  packages = ["."]
  revision = "626af652714cc0092f492644e298e5f3ac7db31a"

[[projects]]
  name = "go.uber.org/atomic"
  packages = ["."]
  revision = "4e336646b2ef9fc6e47be8e21594178f98e5ebcf"
  version = "v1.2.0"

[[projects]]
  name = "go.uber.org/zap"
  packages = [".","buffer","internal/bufferpool","internal/color","internal/exit","internal/multierror","zapcore"]
  revision = "fab453050a7a08c35f31fc5fff6f2dbd962285ab"
  version = "v1.4.0"

[solve-meta]
  analyzer-name = "dep"
  analyzer-version = 1
  inputs-digest = "77d32776fdc88e1025460023bef70534c5457bdc89b817c9bab2b2cf7cccb22f"
  solver-name = "gps-cdcl"
  solver-version = 1

vendor目录下,则是lock文件中各个依赖包的本地clone:

# tree -L 2 vendor
vendor
├── github.com
│   └── beego
└── go.uber.org
    ├── atomic
    └── zap

至此,dep init完毕,相关依赖包也已经被vendor,你可以使用go build/install进行程序构建了。

b)、提交Gopkg.toml和Gopkg.lock

如果你对dep自动分析出来的各种约束和依赖的版本没有异议,那么这里就可以将Gopkg.toml和Gopkg.lock作为项目源码的一部分提交到代码库中了。这样其他人在下载了你的代码后,可以通过dep直接下载lock文件中的第三方包版本,并存在vendor里。这样就使得无论在何处,项目构建的依赖库理论上都是一致的,实现reproduceable build。

是否需要提交vendor下的依赖包代码到代码仓库?这取决于你。提交vendor的好处是即便没有dep,也可以实现真正的reproduceable build。但vendor的提交会让你的代码库变得异常庞大,且更新vendor时,大量的diff会影响到你对代码的review。下面的内容我们以不提交vendor为前提。

c)、dep ensure

现在我们的depdemo已经加入了Gopkg.toml和Gopkg.lock。这时,如果你将depdemo clone到你的本地,你还无法进行reproduceable build,因为这时vendor还不存在。这时我们需要执行下面命令来根据Gopkg.toml和Gopkg.lock中的数据构建vendor目录和同步里面的包:

# dep ensure

# ls -F
Gopkg.lock  Gopkg.toml  main.go  vendor/

ensure成功后,你就可以进行reproduceable build了。

我们可以通过dep status查看当前的依赖情况(包括direct and transitive dependency):

# dep status
PROJECT               CONSTRAINT     VERSION        REVISION  LATEST   PKGS USED
github.com/beego/mux  branch master  branch master  626af65   626af65  1
go.uber.org/atomic    *              v1.2.0         4e33664   4e33664  1
go.uber.org/zap       ^1.4.0         v1.4.0         fab4530   fab4530  7

d) 指定约束

dep init生成的Gopkg.toml中的约束是否是我们预期的呢?这个还真不一定。比如:我们将对zap的约束手工改为1.3.0:

//Gopkg.toml
... ...

[[constraint]]
  name = "go.uber.org/zap"
  version = "<=1.3.0"

执行dep ensure后,查看status:

# dep status
PROJECT               CONSTRAINT     VERSION        REVISION  LATEST   PKGS USED
github.com/beego/mux  branch master  branch master  626af65   626af65  1
go.uber.org/atomic    *              v1.2.0         4e33664   4e33664  1
go.uber.org/zap       <=1.3.0         v1.4.0         fab4530   fab4530  7

不过,此时Gopkg.lock中的zap version依旧是v1.4.0,并没有修改。要想更新lock和vendor下的数据,我们需要给ensure加上一个-update参数:

# dep ensure -update

# git diff Gopkg.lock
diff --git a/depdemo/Gopkg.lock b/depdemo/Gopkg.lock
index fce53dc..7fe3640 100644
--- a/depdemo/Gopkg.lock
+++ b/depdemo/Gopkg.lock
@@ -16,12 +16,12 @@
 [[projects]]
   name = "go.uber.org/zap"
   packages = [".","buffer","internal/bufferpool","internal/color","internal/exit","internal/multierror","zapcore"]
-  revision = "fab453050a7a08c35f31fc5fff6f2dbd962285ab"
-  version = "v1.4.0"
+  revision = "6a4e056f2cc954cfec3581729e758909604b3f76"
+  version = "v1.3.0"

 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "77d32776fdc88e1025460023bef70534c5457bdc89b817c9bab2b2cf7cccb22f"
+  inputs-digest = "b09c1497771f6fe7cdfcf61ab1a026ccc909f4801c08f2c25f186f93f14526b0"
   solver-name = "gps-cdcl"
   solver-version = 1

-update让dep ensure尝试去保证并同步Gopkg.lock和vendor目录下的数据,将Gopkg.lock下的zap的version改为Gopkg.toml下约束的最大值,即v1.3.0,同时更新vendor下的zap代码。

e) 指定依赖

我们也可以直接更新dependency,这将影响Gopkg.lock和vendor下的数据,但Gopkg.toml不会被修改:

# dep ensure 'go.uber.org/zap@<1.4.0'

# git diff
diff --git a/depdemo/Gopkg.lock b/depdemo/Gopkg.lock
index fce53dc..3b17b9b 100644
--- a/depdemo/Gopkg.lock
+++ b/depdemo/Gopkg.lock
@@ -16,12 +16,12 @@
 [[projects]]
   name = "go.uber.org/zap"
   packages = [".","buffer","internal/bufferpool","internal/color","internal/exit","internal/multierror","zapcore"]

-  revision = "fab453050a7a08c35f31fc5fff6f2dbd962285ab"
-  version = "v1.4.0"
+  revision = "6a4e056f2cc954cfec3581729e758909604b3f76"
+  version = "v1.3.0"

 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "77d32776fdc88e1025460023bef70534c5457bdc89b817c9bab2b2cf7cccb22f"
+  inputs-digest = "3307cd7d5942d333c4263fddda66549ac802743402fe350c0403eb3657b33b0b"
   solver-name = "gps-cdcl"
   solver-version = 1

这种情况下会出现Gopkg.lock中的version不满足Gopkg.toml中约束的情况。这里也让我比较困惑!

三、dep探索

上面的dep使用基本工作流完全可以满足日常包管理的需求了。但对于喜欢求甚解的我来说,必要要探索一下dep背后的行为和原理。

1、dep init的两种不同结果

我们回到depdemo的初始状态,即起点:尚未生成dep metadata file的时刻。我们在两种情况下,分别执行dep init:

  • $GOPATH/src下没有go.uber.org/zap
# dep init -v
Searching GOPATH for projects...
  Using master as constraint for direct dep github.com/beego/mux
  Locking in master (626af65) for direct dep github.com/beego/mux
Following dependencies were not found in GOPATH. Dep will use the most recent versions of these projects.
  go.uber.org/zap
Root project is "github.com/bigwhite/experiments/depdemo"
 1 transitively valid internal packages
 2 external packages imported from 2 projects
... ...

# dep status
PROJECT               CONSTRAINT     VERSION        REVISION  LATEST   PKGS USED
github.com/beego/mux  branch master  branch master  626af65   626af65  1
go.uber.org/atomic    *              v1.2.0         4e33664   4e33664  1
go.uber.org/zap       ^1.4.0         v1.4.0         fab4530   fab4530  7

  • $GOPATH/src下存在go.uber.org/zap
# dep init -v
Searching GOPATH for projects...
  Using master as constraint for direct dep github.com/beego/mux
  Locking in master (626af65) for direct dep github.com/beego/mux
  Using master as constraint for direct dep go.uber.org/zap
  Locking in master (b33459c) for direct dep go.uber.org/zap
  Locking in master (908889c) for transitive dep go.uber.org/atomic
Root project is "github.com/bigwhite/experiments/depdemo"
 1 transitively valid internal packages
 2 external packages imported from 2 projects
... ...

# dep status
PROJECT               CONSTRAINT     VERSION        REVISION  LATEST   PKGS USED
github.com/beego/mux  branch master  branch master  626af65   626af65  1
go.uber.org/atomic    *              branch master  908889c   4e33664  1
go.uber.org/zap       branch master  branch master  b33459c   b33459c  7

不知道大家发现两种情况下生成的结果的异同与否。我们只看两个dep status输出中的zap一行:

go.uber.org/zap       ^1.4.0         v1.4.0         fab4530   fab4530  7

vs.

go.uber.org/zap       branch master  branch master  b33459c   b33459c  7

dep自动分析后得到截然不同的两个结果。

第一种情况,我们称之为dep init的network mode,即dep发现本地GOPATH下面没有zap,于是dep init通过network到upstream上查找zap,并“Dep will use the most recent versions of these projects”,即v1.4.0版本。

第二种情况,我们称之为dep init的GOPATH mode, 即dep发现本地GOPATH下面存在zap,于是dep init认定“Using master as constraint for direct dep go.uber.org/zap”,即master branch。

至于为何GOPATH mode下,dep init会选择master,我个人猜测是因为dep觉得既然你本地有zap,那很大可能zap master的稳定性是被你所接受了的。在“dep: updated command spec”中,似乎dep init打算通过增加一个-gopath的flag来区分两种工作模式,并将network mode作为默认工作mode。但目前我所使用的dep版本还没有实现这个功能,其默认工作方式依旧是先GOPATH mode,如果没有找到依赖包的存在,则针对该包实施network mode。

从这里也可以看得出来,对于dep init 输出的约束,你最好还是检视一下,看是否能接受,否则就通过上面提到的“指定约束”来更正dep的输出。

2、dep对项目的依赖包的cache

在进行上面的试验中,我们发现:在本地GOPATH/src下面没有zap的情况下,dep似乎是直接将zap get到本地vendor目录的,而不是先get到GOPATH/src下,在copy到vendor中。事实是什么样的呢?dep的确没有操作GOPATH/src目录,因为那是共享的。dep在$GOPATH/pkg/dep/sources下留了一块“自留地”,用于cache所有从network上下载的依赖包:

# ls -F $GOPATH/pkg/dep/sources/
https---github.com-beego-mux/  https---github.com-uber--go-atomic/  https---github.com-uber--go-zap/

# ls -aF /root/go/pkg/dep/sources/https---github.com-uber--go-zap
./             buffer/            config_test.go   field.go       .gitignore      http_handler.go       LICENSE.txt           options.go          sugar.go       writer.go
../            CHANGELOG.md       CONTRIBUTING.md  field_test.go  glide.lock      http_handler_test.go  logger_bench_test.go  README.md           sugar_test.go  writer_test.go
array.go       check_license.sh*  doc.go           flag.go        glide.yaml      internal/             logger.go             .readme.tmpl        time.go        zapcore/
array_test.go  common_test.go     encoder.go       flag_test.go   global.go       level.go              logger_test.go        stacktrace.go       time_test.go   zapgrpc/
benchmarks/    config.go          encoder_test.go  .git/          global_test.go  level_test.go         Makefile              stacktrace_test.go  .travis.yml    zaptest/

dep对于依赖包的所以git请求均在这个缓存目录下进行。

3、 vendor flatten平坦化

go在1.5加入vendor机制时,是考虑到“钻石形依赖”中存在同一个依赖包的不同版本的。我们来看看dep是否支持这一点。我们设计了一个试验:

img{512x368}

我们建立一个这样的“钻石形”试验环境,foo依赖ab两个包,而a、b两个包分别依赖f的不同版本(通过在a、b中的Gopkg.toml声明这种约束,见图中标注)。

下面是foo项目下面的main.go:

// foo/main.go

package main

import "bitbucket.org/bigwhite/b"
import "bitbucket.org/bigwhite/a"

func main() {
    a.CallA()
    b.CallB()
}

未引入dep前,我们来运行一下该代码:

$go run main.go
call A: master branch
   --> call F:
    call F: v1.1.0
   --> call F end
call B: master branch
   --> call F:
    call F: v2.0.1
   --> call F end

可以看到同样是f包的输出,由于a、b分别依赖f的不同版本,因此输出不同。

我们对foo进行一个dep 分析,看看dep给了我们什么结果:

$dep init -v
Searching GOPATH for projects...
  Using master as constraint for direct dep bitbucket.org/bigwhite/a
  Locking in master (9122a5d) for direct dep bitbucket.org/bigwhite/a
  Using master as constraint for direct dep bitbucket.org/bigwhite/b
  Locking in master (2415845) for direct dep bitbucket.org/bigwhite/b
  Locking in master (971460c) for transitive dep bitbucket.org/bigwhite/f
Root project is "Foo"
 1 transitively valid internal packages
 2 external packages imported from 2 projects
 ... ...

No versions of bitbucket.org/bigwhite/b met constraints:
    master: Could not introduce bitbucket.org/bigwhite/b@master, as it has a dependency on bitbucket.org/bigwhite/f with constraint ^2.0.0, which has no overlap with existing constraint ^1.1.0 from bitbucket.org/bigwhite/a@master
    v2.0.0: Could not introduce bitbucket.org/bigwhite/b@v2.0.0, as it is not allowed by constraint master from project Foo.
    v1.0.0: Could not introduce bitbucket.org/bigwhite/b@v1.0.0, as it is not allowed by constraint master from project Foo.
    master: Could not introduce bitbucket.org/bigwhite/b@master, as it has a dependency on bitbucket.org/bigwhite/f with constraint ^2.0.0, which has no overlap with existing constraint ^1.1.0 from bitbucket.org/bigwhite/a@master

dep init运行失败。由于a依赖的f@^1.1.0和b依赖的f@^2.0.0两个约束之间没有交集,无法调和,dep无法solve这个依赖,于是init failed!

但失败背后还有一层原因,那就是dep的设计要求flatten vendor,即使用dep的项目只能有一个root vendor,所以直接依赖或传递依赖的包中包含vendor的,vendor目录也都会被strip掉。这样一旦依赖包中存在带有冲突的约束,那么dep init必将失败。

四、小结

dep一个重要feature就是支持semver 2.0规范,不过semver的规则好多,不是这里能说清楚的,大家可以到semver官方站细读规则,或者在npm semver calculator这个站点直观感受semver规则带来的变化。

dep试验告一段落。从目前来看,dep已经进入可用阶段,建议有条件的童鞋能积极的使用dep,并为dep进行前期测试,发现问题提issue,为dep的快速完善出出力。

depdemo的代码在这里;a, b,f包的代码在这里这里这里

五、参考资料


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats