前言
Go 的包管理是一直是为人诟病之处,从 Go 1.5 引入的 vendor 机制,到准官方工具 dep,目前为止还没一个简便的解决方案
不过现在 go modules 随着 golang1.11 的发布而和我们见面了,这是官方提倡的新的包管理,乃至项目管理机制,可以不再需要 GOPATH 的存在。
欣喜之余,赶紧上手来试一下吧~
Go modules 的前世今生
说起 Go 的包依赖管理,作为 Google 脑残粉的我也是连连叹气。。。
如果说 Java(Maven) 是坨 shit 的话,那 Go 真是连 Java 都不如。
自 2007 年 “三巨头”(Robert Griesemer, Rob Pike, Ken Thompson)提出设计和实现 Go 语言以来,这门语言已经发展和演化了十余年了。
但是自从 Go 语言诞生以来吧,大佬们就认为go get
已经挺好了,没必要再额外造一个轮子了——包管理器。
也许是大佬们都不需要团队开发吧,但是大佬毕竟是少数,于是,各种各样的社区解决方案就出现了,可谓是百家争鸣。
- dep
- manul - Vendor packages using git submodules.
- Godep
- Govendor
- godm
- govend - Manage dependencies like
go get
but for/vendor
. - Glide - Manage packages like composer, npm, bundler, or other languages.
当初看到这个列表的时候,我不禁感慨:“这么多的解决方案,可真是挑花了朕的眼睛呀。”
Go 官方说:“莫急,这份官方对比拿好不谢。”
Go 在构建设计方面深受 Google 内部开发实践的影响,比如 go get 的设计就深受 Google 内部单一代码仓库 (single monorepo) 和基于主干 (trunk/mainline based) 的开发模型的影响:只获取 Trunk/mainline 代码和版本无感知。
Google 内部基于主干的开发模型:
- 所有开发人员基于主干 trunk/mainline 开发:提交到 trunk 或从 trunk 获取最新的代码(同步到本地 workspace)
- 版本发布时,建立 Release branch,release branch 实质上就是某一个时刻主干代码的快照
- 必须同步到 release branch 上的 bug fix 和增强改进代码也通常是先在 trunk 上提交 (commit),然后再 cherry-pick 到 release branch 上
我们知道 go get 获取的代码会放在 $GOPATH/src 下面,而 go build 会在 $GOROOT/src 和 \$GOPATH/src 下面按照 import path 去搜索 package,由于 go get 获取的都是各个 package repo 的 trunk/mainline 的代码,因此,Go 1.5 之前的 Go compiler 都是基于目标 Go 程序依赖包的 trunk/mainline 代码去编译的。这样的机制带来的问题是显而易见的,至少包括:
- 因依赖包的 trunk 的变化,导致不同人获取和编译你的包 / 程序时得到的结果实质是不同的,即不能实现 reproduceable build
- 因依赖包的 trunk 的变化,引入不兼容的实现,导致你的包 / 程序无法通过编译
- 因依赖包演进而无法通过编译,导致你的包 / 程序无法通过编译
为了实现 reporduceable build,Go 1.5 引入了 Vendor 机制,Go 编译器会优先在 vendor 下搜索依赖的第三方包,这样如果开发者将特定版本的依赖包存放在 vendor 下面并提交到 code repo,那么所有人理论上都会得到同样的编译结果,从而实现 reporduceable build。
在 Go 1.5 发布后的若干年,gopher 们把注意力都集中在如何利用 vendor 解决包依赖问题,从手工添加依赖到 vendor、手工更新依赖,到一众包依赖管理工具的诞生,比如:Govendor、glide 以及号称准官方工具的 dep,努力地尝试着按照当今主流思路解决着诸如 “钻石型依赖” 等难题。
正当 gopher 认为 dep 将 “顺理成章” 地升级为 go toolchain 一部分的时候,今年年初,Go 核心 Team 的技术 leader,也是 Go Team 最早期成员之一的 Russ Cox 在个人博客上连续发表了七篇文章,系统阐述了 Go team 解决” 包依赖管理” 的技术方案: vgo —— modules 的前身。
vgo 的主要思路包括:Semantic Import Versioning、Minimal Version Selection、引入 Go module 等。这七篇文章的发布引发了 Go 社区激烈地争论,尤其是 MVS(最小版本选择) 与目前主流的依赖版本选择方法的相悖让很多传统 Go 包管理工具的维护者” 不满”,尤其是” 准官方工具”:dep。vgo 方案的提出也意味着 dep 项目的生命周期即将进入尾声。
5 月份,Russ Cox 的 Proposal “cmd/go: add package version support to Go toolchain” 被 accepted,这周五早些时候 Russ Cox 将 vgo 的代码 merge 到 Go 主干,并将这套机制正式命名为”go modules”。由于 vgo 项目本身就是一个实验原型,merge 到主干后,vgo 这个术语以及 vgo 项目的使命也就就此结束了。后续 Go modules 机制将直接在 Go 主干上继续演化。
Go modules 是 Go team 在解决包依赖管理方面的一次勇敢尝试,无论如何,对 Go 语言来说都是一个好事。
Go modules 上手
这里就用 Gin 框架来实现两个 RESTful API 作示例吧,目录结构如下,两个 handler 分别属于 main
和 pkg/api/data
这两个 package
1 | mytest |
进入 myproj 目录,然后使用 go mod
建立 modules
1 | $ go mod init github.com/wangjiemin/mytest |
此时会自动产生一个 go.mod 文件,打开看到里边内容只有一行
1 | module github.com/wangjiemin/mytest |
现在开始写我们的两个 handler
1.pkg/api/data/api.go
1 | package data |
2.main.go
1 | package main |
然后用我们的老朋友 go build
创建可执行文件
1 | $ go build -o bin/main main.go |
可以看到 go compiler 主动下载了相关 package。
那么这些 package 被下载到了哪里呢,你打开 $GOPATH/pkg/mod 就可以看到了,另外 modules 是允许同 package 多种版本并存的。
1 | ~/go/pkg/mod |
我们看看执行 go build
后 go.mod 文件的内容:
1 | module github.com/wangjiemin/mytest |
我们看到 go modules 分析出了 mytest 的依赖,并将其放入 go.mod 的 require 区域。
go modules 拉取 package 的原则是先拉取最新的 release tag,若无 tag 则拉最新 commit 并以 Pseudo-versions 的形式记录。
因为我们的 module 只直接依赖了 gin,其他的都是非直接依赖的,所以它们后边都被以注释形式标记了 indirect,即传递依赖。
go.mod 文件一旦创建后,它的内容将会被 go toolchain 全面掌控。go toolchain 会在各类命令执行时,比如 go get、go build、go mod 等修改和维护 go.mod 文件。
同时发现目录下多了一个文件 go.sum
1 | github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= |
写过 node 的人应该会发现, go.mod/go.sum
的关系跟 package.json/package-lock.json
类似,前者定义 dependency root,后者将关系展开。
最后执行 bin/main
可以看到 Gin 很贴心的列出了 handler 所属的 package
1 | $ ./bin/main |
到这里,一个 go modules 就完成了。
GO111MODULE
Modules 是作为 experiment feature 加入到不久前正式发布的 Go 1.11 中的。
按照 Go 的惯例,在新的 experiment feature 首次加入时,都会有一个特性开关,go modules 也不例外,GO111MODULE
这个临时的环境变量就是 go modules 特性的 experiment 开关。
- off: go modules experiment feature 关闭,go compiler 会始终使用 GOPATH mode,即无论要构建的源码目录是否在 GOPATH 路径下,go compiler 都会在传统的 GOPATH 和 vendor 目录 (仅支持在 GOPATH 目录下的 package) 下搜索目标程序依赖的 go package;
- on: 始终使用 module-aware mode,只根据 go.mod 下载 dependency 而完全忽略 GOPATH 以及 vendor 目录
- auto: Golang 1.11 预设值,使用 GOPATH mode 还是 module-aware mode,取决于要构建的源码目录所在位置以及是否包含 go.mod 文件。满足任一条件时才使用 module-aware mode:
- 当前目录位于 GOPATH/src 之外并且包含 go.mod 文件
- 当前目录位于包含 go.mod 文件的目录下
go mod
命令
1 | download download modules to local cache (下载依赖的 modules 到本地 cache) |
看这些命令的帮助已经比较容易了解命令的功能。
既有项目
假设你已经有了一个 go 项目, 比如在$GOPATH/github.com/wangjiemim/mytest
下, 你可以使用go mod init github.com/wangjiemim/mytest
在这个文件夹下创建一个空的 go.mod (只有第一行 module github.com/wangjiemim/mytest
)。
然后你可以通过 go get ./...
让它查找依赖,并记录在 go.mod 文件中 (你还可以指定 -tags
, 这样可以把 tags 的依赖都查找到)。
通过go mod tidy
也可以用来为 go.mod 增加丢失的依赖,删除不需要的依赖,但是我不确定它怎么处理tags
。
执行上面的命令会把 go.mod 的latest
版本换成实际的最新的版本,并且会生成一个go.sum记录每个依赖库的版本和哈希值。
replace
在国内访问golang.org/x
的各个包都需要梯子,你可以在 go.mod 中使用replace
替换成 github 上对应的库。
1 | replace ( |
依赖库中的replac
e对你的主 go.mod 不起作用,比如github.com/wangjiemin/mytest
的 go.mod 已经增加了replace
, 但是你的 go.mod 虽然require
了rpcx
的库,但是没有设置replace
的话, go get
还是会访问golang.org/x
。
所以如果想编译那个项目,就在哪个项目中增加replace
。
包的版本控制
下面的版本都是合法的:
1 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 |
版本号遵循如下规律:
1 | vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef |
也就是版本号 + 时间戳 + hash,我们自己指定版本时只需要制定版本号即可,没有版本 tag 的则需要找到对应 commit 的时间和 hash 值。
另外版本号是支持 query 表达式的,其求值算法是 “选择最接近于比较目标的版本 (tagged version)”,即上文中的 gopkg.in/yaml.v2 会找不高于 v2.2.1 的最高版本。
对于复杂的包依赖场景,可以参考 Russ Cox 在 “Minimal Version Selection” 一文中给出的形象的算法解释 (注意:这个算法仅是便于人类理解,但是性能低下,真正的实现并非按照这个算法实现)。
go get 升级
- 运行
go get -u
将会升级到最新的次要版本或者修订版本 (x.y.z,z 是修订版本号, y 是次要版本号) - 运行
go get -u=patch
将会升级到最新的修订版本 - 运行
go get package@version
将会升级到指定的版本号version
go modules 与 vendor
在最初的设计中,Russ Cox 是想彻底废除掉 vendor 的,但在社区的反馈下,vendor 得以保留,这也是为了兼容 Go 1.11 之前的版本。
Go modules 支持通过go mod vendor
命令将某个 module 的所有依赖保存一份 copy 到 root module dir 的 vendor 下,然后在构建的使用go build -mod=vendor
即可忽略 cache 里的包而只使用 vendor 目录里的版本。
参考
- 语义化版本 2.0.0 | Semantic Versioning
- go - The Go Programming Language
- Modules · golang/go Wiki · GitHub
- Introduction to Go Modules – Roberto Selbach
- 初窥 Go module | Tony Bai
- Golang 1.11 新功能介紹 - Modules - LightBlue Essay
- Golang modules 初探 - Go 中国技术社区 - golang
- 跳出 Go module 的泥潭 | 鸟窝
- Go 包管理解决之道 —— Modules 初试