包管理器
随着软件规模的越来越大,引入的第三方包就会越来越多,包之间的依赖关系也会越来越复杂,容易造成“依赖地狱”问题。包管理器(package manager)可以让依赖包管理更加简单、可靠且高效,目前主流编程语言都会提供包管理器,例如Python的pip、Node.js的npm(Node Package Manager)和yarn(Yet Another Resource Negotiator)、Java的maven、Rust的cargo、PHP的composer、Ruby的gem和bundler、.NET的nuget、C/C++的conan和vcpkg、Go的go mod。
Dependency hell is a colloquial term for the frustration of some software users who have installed software packages which have dependencies on specific versions of other software packages.
说明:包管理器是一个通用概念,除了编程语言会提供包管理器,操作系统也会提供包管理器,例如Ubuntu的apt、CentOS的yum。
语义化版本
语义化版本(Semantic Versioning)可以让版本管理更加简单,同时也保证使用者能够通过版本号理解新发布版本的意义。语义化版本号格式如下
<MAJOR>.<MINOR>.<PATCH> // 1.0.0
<MAJOR>.<MINOR>.<PATCH>-<PRE-RELEASE> // 1.0.0-alpha
<MAJOR>.<MINOR>.<PATCH>+<BUILD> // 1.0.0+20130313144700
<MAJOR>.<MINOR>.<PATCH>-<PRE-RELEASE>+<BUILD> // 1.0.0-alpha+001
- MAJOR:主版本号(做了不向下兼容的API修改时递增)---非负整数,且禁止在数字前方补零
- MINOR:次版本号(做了向下兼容的功能新增时递增)---非负整数,且禁止在数字前方补零
- PATCH:修订版本号(做了向下兼容的问题修复时递增)---非负整数,且禁止在数字前方补零
- PRE-RELEASE:先行版本号(非稳定版而且可能无法满足预期的兼容性需求)---以句点分隔的标识符,标识符必须由ASCII字母数字和连接号 [0-9A-Za-z-] 组成(字符串)
- BUILD-METADATA:版本编译信息(判断版本的优先层级时,版本编译信息可被忽略)---以句点分隔的标识符,标识符必须由ASCII字母数字和连接号 [0-9A-Za-z-] 组成(字符串)
说明1:标记版本号的软件发行后,禁止改变该版本软件的内容,任何修改都必须以新版本发行。immutable snapshot
说明2:主版本号不同的两个版本是相互不兼容的。在主版本号相同的情况下,次版本号大都是向后兼容次版本号小的版本。修订版本号也不影响兼容性。
说明3:0.1.0作为初始开发版本(0.y.z为非稳定版,不保证兼容性),1.0.0作为第一个正式发布版本(稳定版)
说明4:主版本号为0、具有先行版本号的版本均为不稳定版本
“v1.2.3” 是一个语义化版本号吗?
“v1.2.3” 并不是一个语义化的版本号。但是,在语义化版本号之前增加前缀 “v” 是用来表示版本号的常用做法。在版本控制系统中,将 “version” 缩写为 “v” 是很常见的。比如:git tag v1.2.3 -m "Release version 1.2.3" 中,“v1.2.3” 表示标签名称,而 “1.2.3” 是语义化版本号。
检查语义化版本号的正则表达式详见regex101.com/r/Ly7O1x/3/
Go包管理器
发展历史
- 2012.03 gopath模式(Go 1.0,全局共享模式)---go get仅仅支持获取master branch上latest代码,没有指定version、branch或revision的能力。没有版本控制,不同时间获取的依赖包版本可能不一致,无法实现重现构建(reproducible build),即不保证基于同一源码构建出的可执行文件是等价的
- 2013.10 godep(社区工具,5.6k)---Godeps/Godeps.json和Godeps/_workspace目录(新版本为vendor目录)
- 2014.07 glide(社区工具,8.2k)---glide.yaml、glide.lock和vendor目录
- 2015.08 vendor机制(Go 1.5,GO15VENDOREXPERIMENT=1,项目独享模式)---每个项目增加一个vendor子目录存放第三方依赖包,编译器优先感知和使用vendor目录下的依赖包版本。标准化存放位置,但无法自动管理依赖包版本;需要将vendor提交到代码仓库中,占用代码仓库空间,减慢更新速度
- 2016.08 govendor(社区工具,4.9k)---vendor/vendor.json和vendor目录
- 2017.08 dep(准官方工具,12.9k)---Gopkg.toml、Gopkg.lock和vendor目录
- 2018.01 vgo(go module的前身,1.5k)
- 2018.08 go module(Go 1.11,GO111MODULE=on)---go.mod和go.sum
go module是Go官方推出的依赖包管理工具,Go 1.11版本正式发布,Go 1.14版本生产可用。go module集成在Go工具链中,只要安装Go语言就可以使用。一个模块(module)是由一组相关包(package)组成的一个独立的版本管理单元,每个模块有一个go.mod文件用于定义模块名称、依赖包及其版本。
A module is a collection of Go packages stored in a file tree with a go.mod file at its root. The go.mod file defines the module’s module path, which is also the import path used for the root directory, and its dependency requirements, which are the other modules needed for a successful build. Each dependency requirement is written as a module path and a specific semantic version.
环境变量
- GO111MODULE:管理模式(module-aware模式或gopath模式),可选值为auto、on和off。auto时管理模式取决于项目目录所在位置以及是否包含go.mod文件
- GOMODCACHE:模块缓存目录,默认为$GOPATH/pkg/mod
- GOPROXY:模块代理(web server)的服务地址,支持设置多个模块代理(采用逗号分隔)。direct表示回源到源地址拉取,off表示禁止从任何源下载模块
仓库源:从VCS(version control system,版本控制系统)下载,需要安装VCS工具
镜像源:从代理服务器下载,支持https/http/file协议。代理服务器一旦缓存就无法修改和删除(即使源码仓库被删除,也不会主动删除)
- GOSUMDB:校验和数据库(web server)的服务地址和可选的公钥,off表示禁止校验和数据库的安全校验,仅使用本地的go.sum进行安全校验
- GOPRIVATE:私有模块,支持通配符。同时作为GONOPROXY和GONOSUMDB的默认值
- GONOPROXY:不使用GOPROXY下载的私有模块,支持通配符
- GONOSUMDB:不使用GOSUMDB验证的私有模块,支持通配符
- GOPATH:工作目录(workspace),可以设置多个目录,每个目录包含src、bin和pkg三个子目录。src存放源码文件,bin存放编译生成的可执行文件(不会创建额外子目录),pkg存放编译生成的目标文件,以加快程序的后续编译速度(创建子目录$GOOS_$GOARCH)。Linux下默认为$HOME/go,Windows下默认为%USERPROFILE%\go
gopath模式下GOPATH两种特殊用法
方式1:GOPATH设置两个目录,第一个目录专门用于下载第三方依赖包(go get下载第三方依赖包时默认保存到GOPATH第一个工作目录内),第二个目录用于内部工程目录,避免外部代码和项目代码互相干扰。
方式2:GOPATH设置一个目录,每个项目设置为不同目录(通过脚本工具在激活项目时自动设置相关环境变量)。export GOPATH="pwd:$GOPATH"
管理模式
说明:启用go module之后,虽然不需要在$GOPATH/src中创建项目,但是GOPATH依然存在,它在Go工具链中依旧发挥着至关重要的作用。
Is the GOPATH variable being removed?
No. The GOPATH variable (set in the environment or by go env -w) is not being removed. It will still be used to determine the default binary install location, module cache location, and checksum database cache location, as mentioned at the top of this page.
模块代理
知名模块代理
- proxy.golang.org: 官方提供的模块代理服务(默认)
- proxy.golang.com.cn: 中国Go语言贡献者组织提供的模块代理服务
- mirrors.tencent.com/go: 腾讯云提供的模块代理服务
- mirrors.aliyun.com/goproxy: 阿里云提供的模块代理服务
- goproxy.cn: 开源模块代理服务,由七牛云提供,是目前中国最为稳定的模块代理服务
- goproxy.io: 开源模块代理服务,由中国Go社区提供
- Athens: 开源模块代理服务,可基于该代理自行搭建模块代理服务
访问模块代理
通过以下命令可以从代理服务器获取go.mod和$version.zip。
curl https://goproxy.cn/golang.org/x/mod/@v/v0.2.0.mod
curl -O https://goproxy.cn/golang.org/x/mod/@v/v0.2.0.zip
curl https://goproxy.cn/sumdb/sum.golang.org/lookup/golang.org/x/mod@v0.2.0
curl https://sum.golang.google.cn/lookup/golang.org/x/mod@v0.2.0
模块缓存
启用go module后依赖包会下载到$GOPATH/pkg/mod中,并且带版本号,同一个版本只缓存一份,多个项目可以共享。为了避免已缓存的模块被更改,$GOPATH/pkg/mod下的依赖包是只读的,不允许修改。
操作命令
download download modules to local cache. 下载go.mod文件记录的所有模块到本地缓存(默认为$GOPAH/pkg/mod)
edit edit go.mod from tools or scripts. 编辑go.mod文件
graph print module requirement graph. 查看模块依赖图,包括直接依赖和间接依赖
init initialize new module in current directory. 初始化当前目录为一个新模块,创建go.mod文件。如果go.mod文件已经存在,则初始化失败
tidy add missing and remove unused modules. 增加缺少的模块,移除无用的模块,更新go.mod文件和go.sum文件!!!
vendor make vendored copy of dependencies. 复制依赖包到vendor目录。如果没有第三方依赖包,则不会创建vendor目录
verify verify dependencies have expected content. 校验本地缓存中的依赖包是否被修改
why explain why packages or modules are needed. 查看为什么需要包或模块
说明1:Go官方工具链中所有命令都内置对go module的支持,比如go get、go build都可能会修改go.mod文件和go.sum文件。
说明2:go module本身就可以实现可重现构建而不需要vendor目录,仅仅为了兼容vendor机制。
配置文件
go.mod文件
记录模块属性,包括模块路径、第三方依赖包及其版本号,Go工具链根据go.mod文件下载依赖包。
说明:go.mod文件在项目根目录下只需要创建一次即可,子目录下不需要重复创建go.mod文件。根目录及其子目录下的所有包同属于一个模块,该模块中的包在被导入的时候,包导入路径使用“module/package”。
1、module directive(模块路径)
语法:module module-path
示例:
module github.com/panicthis/modfile # 主版本号为0或1时,必须省略主版本号
module github.com/panicthis/modfile/v2 # 主版本号大于等于2时,必须增加主版本号
模块路径(module path)作用:(1)作为包导入路径(import path)前缀(2)定位代码仓库的网络位置
Each module's path not only serves as an import path prefix for its packages, but also indicates where the go command should look to download it. For example, in order to download the module golang.org/x/tools, the go command would consult the repository indicated by golang.org/x/tools (described more here).
An import path is a string used to import a package. A package's import path is its module path joined with its subdirectory within the module. For example, the module github.com/google/go-cmp contains a package in the directory cmp/. That package's import path is github.com/google/go-cmp/cmp. Packages in the standard library do not have a module path prefix.
2、go directive(Go版本指示符)
语法:go minimum-go-version
示例:go 1.16
3、require directive(必需模块)
语法:require module-path module-version [//indirect]
示例:require golang.org/x/net v1.2.3
-
incompatible后缀:不兼容标识,例如主版本号大于等于2,但模块路径没有添加v2、v3等后缀
-
indirect注释:间接依赖,即依赖引入的依赖。默认go.mod文件只会出现直接依赖,下列几种特殊情况会出现间接依赖,并加上 // indirect 进行标记
情形1:手动指定了更高的依赖版本,比如在不引用 golang.org/x/text 的前提下通过 go get golang.org/x/text@v0.3.2 升级依赖
情形2:依赖库还没有切换到 go module,这时候 go 工具链是不知道内部的依赖关系的,所以所有的依赖都会直接添加到当前模块中
canonical version
-
正规版本号:vmajor.minor.patch
-
伪版本号(pseudo-version):vmajor.minor.patch-timestamp- commithash
说明:伪版本号是一种特殊的先行版本号,由go module生成的一个类似符合语义化规范的版本号,实际这个库并没有发布这个版本。
non-canonical version
-
major或major.minor
-
commitid或HEAD(最新的commitid)
-
master:选择master分支的最新commit
-
latest: 选择最高的release版本,如果没有release版本,则选择最高的pre-release版本,如果根本就没有打过tag,则选择最高的伪版本号的版本(默认分支的最后的提交版本)
-
upgrade: 类似latest,但是如果有比release更高的版本(比如pre-release),会选择更高的版本
-
patch: major和minor和当前的版本相同,只把patch升级到最高。当然如果没有当前的版本,则无从比较,则patch退化成latest语义
-
none:移除依赖包
说明:go.mod文件中可以使用non-canonical version,但通过go get或go mod tidy命令会尝试将non-canonical version转为canonical version,让依赖包的版本对应一个确定的版本,否则master、HEAD在不同的人使用的时候可能会对应不同的版本。
4、exclude directive(排除模块)
语法:exclude module-path module-version
示例:exclude golang.org/x/net v1.2.3
排除某个依赖包的特定版本,例如排除某个有严重问题的版本。
5、replace directive(替换模块)
语法:replace module-path [module-version] => replacement-path [replacement-version]
将某个依赖包替换为另一个依赖包,包导入路径不变。
- 替换为其他依赖包路径(方便下载的包或者内部私有仓库)
- 替换为本地目录(用于开发调试),可以是绝对路径,也可以是相对路径
6、retract directive(撤回模块)
语法:retract version|[version-low,version-high] // rationale
标识某些版本是作废的且不推荐使用的 。retract是由维护者定义, 而exclude是由使用者定义。
说明:replace 和 exclude 只作用于当前模块的构建,它们既不会向上继承,也不会向下传递。
go.sum文件
记录每个依赖包的版本号和哈希值(如果没有第三方依赖包,则go.sum文件不存在)。正常情况下,每个依赖包会包含两条记录:依赖包所有文件的哈希值和该依赖包 go.mod 的哈希值。
<path> <version>[/go.mod] <algorithm>:<hash> // h1表示SHA-256哈希算法
执行构建时,如果依赖包的哈希值与go.sum文件记录的哈希值(或从校验和数据库查询的哈希值)不一致,就会拒绝构建。
If the go.sum file is not present, or if it doesn’t contain a hash for the downloaded file, the go command may verify the hash using the checksum database, a global source of hashes for publicly available modules. Once the hash is verified, the go command adds it to go.sum and adds the downloaded file in the module cache. If a module is private (matched by the GOPRIVATE or GONOSUMDB environment variables) or if the checksum database is disabled (by setting GOSUMDB=off), the go command accepts the hash and adds the file to the module cache without verifying it.
说明:go.sum文件只是一个元信息数据库,随着项目依赖的演进与变更,go.sum文件中会存储一个module的多个版本信息,即使某个版本已经不再被当前module所依赖(通过go mod tidy清理)。
工作原理
语义导入版本(Semantic Import Versioning)
主版本号作为包导入路径的一部分,支持同一个源码文件中导入同一个包的不同版本(多版本共存):包名相同,但导入路径不同。主版本号为0或1时,包导入路径省略版本号信息。主版本号大于等于2时,包导入路径需要增加v2、v3等后缀。
说明:根据语义化版本的要求,v0 是不需要保证兼容性的,可以随意的引入破坏性变更,所以不需要显式的写出来;而省略 v1 更大程度上是现实的考虑,毕竟 99% 的包都不会有 v2,同时考虑到现有代码库的兼容,省略 v1 是一个合情合理的决策。
module path: module github.com/my/mod/v2
require: require github.com/my/mod/v2 v2.0.0
import: import "github.com/my/mod/v2/mypkg"
最小版本选择(Minimal Version Selection)
选择依赖的版本时,默认选择符合项目整体要求的 “最小版本”。可以执行带版本号的go get命令或修改go.mod文件显式指定版本。
当前其他主流编程语言,以及go module之前的依赖管理工具都会选择依赖项的“最新最大 (Latest Greatest) 版本”。
备注:最新最大版本在不同时刻可能是不同的,那么在不同时刻的构建产生的最终文件就是不同的。
代码示例
$ mkdir -p /home/gopher/hello;cd /home/gopher/hello # 创建项目目录($GOPATH/src之外)
$ vim hello.go
package main
import (
"fmt"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Hello())
}
$ go version # 确保Go 1.11及以上
$ go env -w GO111MODULE=on # 开启module-aware模式,也可以直接设置export GO111MODULE=on
$ go env -w GOPROXY="https://goproxy.cn,direct" # 设置模块代理
$ go env -w GOSUMDB=off #
$ go env # 查看环境变量
$ go clean -modcache # 清空缓存区(可以不执行),tree -L 2 /root/go/pkg/
$ go mod init github.com/my/repo # 初始化并创建go.mod文件
go: creating new go.mod: module github.com/my/repo
$ cat go.mod
module github.com/my/repo
go 1.13
$ go mod tidy # 更新go.mod文件和go.sum文件(两个文件与源码一起提交到代码仓库)
$ cat go.mod
module github.com/my/repo
go 1.13
require rsc.io/quote v1.5.2
$ cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
$ go list -m all # 查看所有依赖
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: finding rsc.io/sampler v1.3.0
github.com/my/repo
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$ go build -o hello # 构建(编译和链接)
$ ./hello
Hello, world.
# go module中使用vendor机制
go mod vendor # 复制依赖包到vendor目录,vendor/modules.txt记录模块及其版本
go build -mode=vendor -o hello # 基于vendor构建,而不是基于模块缓存构建
常见问题
(1)重置依赖关系
$ rm go.*
$ go mod init module-path
$ go mod tidy
(2)go get升级
go get -d -u ./... // 升级所有模块。go get xxxxx/...
go get -d -u path // 升级次版本号或修订版本号
go get -d -u=patch path // 升级修订版本号
go get -d -u path@version // 升级指定版本号。如果升级主版本号,包导入路径中加上vN,代码中针对不兼容的部分进行修改
The -d flag instructs get to download the source code needed to build the named packages, including downloading necessary dependencies, but not to build and install them.
The -u flag instructs get to update modules providing dependencies of packages named on the command line to use newer minor or patch releases when available.
The -u=patch flag (not -u patch) also instructs get to update dependencies, but changes the default to select patch releases.
(3)如何彻底移除依赖包?
- 源码中删除依赖包相关代码
- 执行go mod tidy
(4)如何导入本地模块?
require github.com/user/repo v1.0.0
replace github.com/user/repo v1.0.0 => 本地源码路径
(5)go.mod文件修改方式?
- go get命令自动修改(升降级)
- go mod tidy命令自动修改(扫描源码)
- go mod edit
- 手动修改(go mod edit -fmt格式化)
(6)go.sum文件要不要提交到代码仓库?需要!!!如果不提交go.sum文件,当GOSUMDB=off时,无法校验依赖包是否被更改。
Both go.mod and go.sum should be checked into version control.
(7)如何查看可用版本?
go list -m -versions golang.org/x/text
参考资料
go help mod
《Tony Bai · Go语言第一课》
《Go 语言项目开发实战》
《Go进阶 · 分布式爬虫实战》