[Golang]包管理

本文是本人在探索 Go 最新的包管理 Go Modules 的一些总结,希望能够更深入了解 Go 最新的包管理方式,以及在实际环境中将它很好的使用起来。
1 GOPATH vs Go Modules
在 Go1.5 之前用 GOPATH 以及 GOROOT 这两个环境变量来管理包的位置,GOROOT 为 Go 的安装目录,以及编译过程中使用到的系统库存放位置,如fmt。Go1.5 到 Go1.7 开始稳定到 Vendor 方式,即依赖包需要放到 $GOPATH/src/vendor 目录下,这样每个项目都有自己的 vendor 目录,但是如果依赖同样的三方包,很容易造成资源重复,Go vendor 出现了几种主流的管理工具,包括 godep、govendor、golide等。
在 Go1.11 之前,GOPATH 是开发时的工作目录,其中包含三个子目录:
- src目录:存放go项目源码和依赖源码,包括使用
go get下载的包 - bin目录:通过使用
go install命令将go build编译出的二进制可执行文件存放于此 - pkg目录:go源码包编译生成的lib文件存储的地方
在 Go1.11 之前,import 包时的搜索路径
GOROOT/src: 该目录保存了Go标准库代码(首先搜寻导入包的地方)GOPATH/src: 该目录保存了应用自身的各个包代码和第三方依赖的代码./vendor:vendor 方式第三方依赖包(如果支持Vendor)
在 Unix 和类 Unix 系统上,GOPATH 默认值是 $HOME/go,Go1.11 版本后,开启 GO Modules 后,GOPATH的作用仅仅为存放依赖的目录了。
在 Go 的 1.11 版本之前,GOPATH 是必需的,且所有的 Go 项目代码都要保存在 GOPATH/src 目录下,也就是如果想引用本地的包,你需要将包放在 $GOPATH/src 目录下才能找得到。Go 的 1.11 版本之后,GO 官方引入了 Go Modules,不仅仅方便的使用我们的依赖,而且还对依赖的版本进行了管理。
在Go1.11后通过 go mod vendor 和 -mod=vendor来实现 Vendor 管理依赖方式。本来在 vgo 项目(Go Modules前身)是要完全放弃 vendor,但是在社区反馈下还是保留了。总之就是在 Go.1.11 之后需要开启 Go Modules 条件下才能使用 Vendor,具体地感兴趣或还沿用了 Vendor 的朋友可以去了解下,不过建议以后仅使用 Go Modules 包管理方式了。
2 Go Modules、Go Module Proxy 和 goproxy.cn
Go Modules 是 Go 1.11 推出的功能模块,前身是 vgo,成长于 Go 1.12,丰富于 Go 1.13 是 Go 更好的一种模块依赖管理解决方案实现。
而 Go Module Proxy 是随着 Go Modules 一起产生的模块代理协议,通过这个协议,我们可以实现 Go 模块代理,通过镜像网站下载相关依赖模块。
proxy.golang.org 为 Go 官方模块代理网站,不FQ中国用户是无法访问的,而 goproxy.cn (官方推荐是使用 Go1.13 或以上版本)是七牛云推出的非盈利性 Go 模块代理网站,为中国和世界上其他地方的 Gopher 们提供一个免费的、可靠的、持续在线的且经过 CDN 加速的模块代理,添加这个代理很简单:
# 开启 GO Modules 包管理方式
$ go env -w GO111MODULE=on
# 设置代理为 https://goproxy.cn
# 你也可以设置多个代理,通过逗号分隔开,模块从左至右设置的代理中查找获取
$ go env -w GOPROXY=https://goproxy.cn,direct
注意:模块可能是一个项目,项目下面可以包含很多包。
3 Go Modules 相关知识
3.1 语义化版本控制规范
Go Modules 是如何实现版本控制的呢?通过强制使用语义化版本控制规范,详见 https://semver.org/lang/zh-CN/ 示例,即我们发布版本的时候必须按照官方指定的版本命名格式来发布,具体的:
- 你的版本
Tag没有遵循语义化版本控制规范那么它就会忽略你的Tag,然后根据你的Commit时间和哈希值再为你生成一个假定的符合语义化版本控制规范的版本号,比如v0.0.1-20180523231146-b3f5c0f6e5f1。- 如v0.1.0,v1.0.0,v1.5.0-rc.1,
v这个字符是必须的
- 如v0.1.0,v1.0.0,v1.5.0-rc.1,
Go Modules默认认为,只要你的主版本号不变,那这个模块版本肯定就不包含重大变更,则我们import的时候path不会受到影响,比如v1.0.0和v2.0.0,就是一个重大版本变更,在编写代码 import 模块的时候,v1版本的包名是github.com/xx/xx,v2版本的包名就是github.com/xx/xx/v2了,在我们使用go get的时候也需要带上完整的版本路径才能导入指定的版本。
3.2 go.mod
一个模块是通过go.mod来定义的,也是标志该项目是否启用了 Go Modules,如果存在该文件,默认则启动 Go Modules,除非你设置 GO111MODULE=off。该文件描述了该模块的依赖、不依赖、依赖替换、当前模块名称(路径)、所要求的Go版本信息,示例:
module my/thing
go 1.12
require other/thing v1.0.2
require new/thing/v2 v2.3.4
// 注释:也可以用块结构设定多个依赖模块
require (
new/thing v2.3.4
old/thing v1.2.3
github.com/my/repo v0.0.1-20180523231146-b3f5c0f6e5f1
)
exclude old/thing v1.2.3
replace bad/thing v1.4.5 => good/thing v1.4.5
其中:
- module, 定义模块的路径(名称)
- go, 设置期望的Go版本
- require, 在给定的版本或者更高的版本模块中,指定依赖一个特定版本
- exclude, 排除特定模块版本依赖
- replace, 将指定模块版本替换为其他模块版本
require 和 replace 仅仅在主模块的 go.mod 中应用,在依赖模块的 go.mod 中的 require 和 replace 将会忽略。另// indirect,表示非直接依赖。go build、go get、go install、go list、go test、go mod tidy、go mod why 这些命令会去检测本地模块的引用和存在,如果不存在会去下载相应模块,然后更新记录到 go.mod 文件。
replace 具体的作用就是将一个模块版本替换为另一个模块版本, => 标志前是待替换版本。
3.3 go.sum
go.sum 文件的作用是为了验证每个下载的模块是否与过去下载的模块匹配,并检测模块是否被恶意篡改。比如你在开发过程中依赖了一个模块的某个版本,完成开发后,你上层版本管理平台时只有go.mod和go.sum,如果其他人去使用该项目或者基于该项目开发,则需要在他本地重新下载相应的模块,这时go.sum里记录的加密校验和就可以校验新环境下下载的模块是否与原始依赖保持一致。
在每一个模块的根目录都有一个go.sum与go.mod相匹配,记录go.mod中每一个依赖模块的加密校验和,校验和的前缀是h<N>,h1表示采用SHA-256算法得到校验和,go.sum的每一行格式为:
<模块路径> <版本>[/go.mod] <校验和>
// 示例:
// cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
// github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
// golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
如果下载的模块没有包含在 go.sum 中,而且是一个公共可获得的模块,Go 命令会去 Go 校验数据库(如默认配置的 sum.golang.org 或中国大陆官方校验和数据库 sum.golang.google.cn )查询并获取该模块的校验和,如果下载的模块代码与校验和不匹配,则报告不匹配相关信息并退出,如果匹配,则将校验和写入 go.sum文件中。
3.4 GOPROXY、GONOPROXY、GOSUMDB、GONOSUMDB、GOPRIVATE
Go 命令可以根据 GOPROXY 环境变量的设置,从代理获取模块或直接连接到源代码管理服务器。GOPROXY 的默认设置是 https://proxy.golang.org,direct,这意味着尝试获取 Go 模块镜像,如果代理报告它没有该模块(HTTP错误404或410),则返回直接连接。如果 GOPROXY 设置为 "direct" 字符串,则直接连接到源代码管理服务器下载模块。将 GOPROXY 设置为 "off" 不允许从任何源下载模块。你也可以设置多个代理,通过逗号(,) 或者管道符号(|)分隔开,模块从左至右设置的代理中查找获取,直到获取模块成功或失败返回。
通过设置 GOSUMDB 环境变量,可以配置模块校验数据库,如:
GOSUMDB="sum.golang.org" # 默认配置,URL 默认都是 https://,后跟数据库地址
GOSUNDB="sum.golang.google.cn" # 中国大陆可访问
GOSUMDB="sum.golang.org+<publickey>" # 使用除了sum.golang.org 和 sum.golang.google.cn 域名外其他需要给出公钥
GOSUMDB="sum.golang.org+<publickey> https://sum.golang.org" #
GOSUMDB="off" # 关闭校验,任何模块可以被使用
Go 命令默认是从公共的镜像下载代理网站 proxy.golang.org 下载代码,然后通过公共校验和数据库 sum.golang.org 获取模块校验和实现校验,但是有时候公司需要实现私有化依赖,即可以控制哪些模块可以不使用公共代理或校验数据库。
# 任何匹配*.corp.example.com为前缀的模块都被视为私有模块,包括如git.crop.example.com/xxx, rsc.io/private/yyy
# 配置GORPIVATE过滤规则时,通过逗号分隔配置多个匹配路径
$ go env -w GOPRIVATE=*.corp.example.com,rsc.io/private
为了对模块的下载和校验进行细粒度的控制,GONOPROXY 和 GONOSUMDB 环境变量也支持同 GOPRIVATE 同样的列表设置方式,也是配置统配模块或者指定模块,从而覆盖 GOPRIVATE 对相关模块的作用,如果 GONOPROXY 设置成 none,则所有的模型(公有,私有)都将从 GOPROXY 代理上下载,即 GOPRIVATE 设置无法生效。如:
GOPRIVATE=*.corp.example.com
GOPROXY=proxy.example.com
GONOPROXY=none
如果想要将某个模块不从 GOPROXY 中查找下载,则设置 GONOPROXY 即可,并且也不校验该模块,如:
GOPROXY=https://proxy.golang.org
GONOPROXY=gitlab.com/xxx
GONOSUMDB=$GONOPROXY
如果想禁止从 GOPROXY 上查找下载模块,则可以配置 GONOPROXY=* 或者 GOPROXY=off,不过这样设置不会关掉对模块的校验。
注: GOPRIVATE 、GONOSUMDB、GONOPROXY 的通配配置规则同Linux glob通配符语法一致,如
*表示匹配任意长度任意字符串。
4 Go Modules 及其相关常用命令
go get命令会下载给定的导入模块路径所有的包,包括包的依赖模块
# 将会升级到最新的次要版本或者修订版本(x.y.z, z是修订版本号, y是次要版本号)
$ go get -u [URL]
# 将会升级到最新的次要版本
$ go get -u=patch [URL]
# 将不会校验校验码,同 GOSUMDB=off 效果一致;另外可以下载来自非https域名的模块
$ go get -insecure [URL]
# 下载指定版本的模块,如最新版本是v2.2.0,将级下载v2.1.0
$ go get github.com/urfave/cli/v2@v2.1.0
# 拉取master分支最新提交
$ go get github.com/my/repo@master
# 拉取某个指定的提交
$ go get github.com/my/repo@772611b
go list -m all: 查看在编译过程使用到所有直接和间接依赖项的最终版本go list -m -u all: 查看在编译过程使用到所有直接和间接依赖项的最终版本以及他们可升级的次要的(minor)或补丁(patch)版本go get -u ./...或go get -u=patch ./...: 在模块根目录执行,将所有直接和间接依赖项更新为最新的次要(minor)或补丁(patch)版本go build ./...或go test ./...: 在模块根目录执行,编译或测试模块中的所有包go clean -modcache:删除下载的缓存内容,默认目录为$HOME/go/mod,整个目录会删除掉
注:如果没有 go.mod 文件,
go get下载依赖后不会将版本依赖信息记录到go.mod中。
go mod 相关:
$ go mod download 下载依赖的module到本地cache
$ go mod edit 编辑go.mod文件
$ go mod graph 打印模块依赖图
$ go mod init 在当前文件夹下初始化一个新的module, 创建go.mod文件
$ go mod tidy 增加丢失的module,去掉未用的module
$ go mod vendor 将依赖复制到vendor目录下
$ go mod verify 校验依赖
$ go mod why 解释为什么需要依赖
具体使用:
$ go mod download [-x] [-json] [modules]- 默认下载主模块依赖的所有模块到本地缓存目录中(默认为
$HOME/go/pkg/mod/cache) -x打印下载过程中执行的命令-json将一系列json对象打印到标准输出,描述每个下载的模块信息,包括是否失败、版本、模块路径、校验和值等
- 默认下载主模块依赖的所有模块到本地缓存目录中(默认为
$ go mod verify- 验证检查当前模块(存储在本地下载的源缓存中)的依赖项在下载后是否未被修改。如果所有模块都未修改,verify会打印“all modules virfied”,否则它会报告哪些模块已更改,并导致“go mod”以非零状态退出
$ go mod edit [editing flags] [go.mod]- 主要是在命令行操作编辑go.mod文件
-fmt标志表示格式化go.mod文件,不做除此之外其他更改操作-module=new-module-path标志 : 更改主模块的路径(项目名称),即第一行的module内容-require=path@version和-droprequire=path: 添加和删除require()内容,但一般添加依赖我们更常用go get将依赖自动更新到go.mod中-exclude=path@version和-dropexclude=path@version:添加和删除exclude内容,如果以及添加已经存在,则不做任何操作。-replace=old[@v]=new[@v]: 将旧模块替换为新模块-go=version: 设置预期的Go语言版本-print: 按格式化打印 go.mod 内容,不对 go.mod 做任何修改-json: 按json格式打印 go.mod内容,如果需要知道项目的所以依赖用go list -m -json all
$ go mod graph- 打印所有模块的依赖关系,除了主模块,其他模块依赖关系都带有具体版本信息
$ go mod init [module]- 在当前目录下创建一个模块路径(模块名)为
[moudle]的go.mod,如果已经存在,则提示已经存在。
- 在当前目录下创建一个模块路径(模块名)为
$ go mod tidy [-v]- 确保 go.mod 与 源代码匹配,会添加当前编译过程中包或者其他依赖所缺少的模块,会删除没有提供任何包无用的模块,还会添加一些缺失的校验信息到 go.sum中,移除无用的校验信息
-v标志将 tidy 过程中已删除(没有使用到)的模块信息打印到标准错误
$ go mod vendor [-v]- 这个命令是重置我们主模块vendor目录,将所以编译和测试依赖的包(不包括测试代码)全部拷贝一份到vendor目录。
-v标志打印执行命令过程被拷贝的模块和包的名称到标准错误
$ go mod why [-m] [-vendor] packages- 显示出
go mod graph依赖关系中的一个最短依赖关系,比如go mod graph展示出主模块依赖子模块1,子模块1依赖子模块2,则会全部展示,而如果想查某个模块或某些模块依赖了哪些,则可以用go mod why。
- 显示出
5 Go Modules 实践
5.1 创建一个 Go Modules 项目
Go 官方 FAQs 上提到我们的项目没有任何模块依赖是否有必要去添加一个 go.mod 文件呢?它的建议是有必要的,这可以让我们不再依赖 GOPATH环境变量,也有利于模块的生态系统发展和交流,另外也可以作为你项目的一个声明标志,不过一切都是基于在 GO1.11 版本之上。那如何创建一个 Go Modules 项目呢?
- 首先我们要求
Go1.11版本或以上,建议使用Go 1.13版本或以上 - 进入我们项目的根目录
$ cd <project path>
- 我们无需设置
GO111MODULE环境变量,执行$ go mod init [your module path],如$ go mod init github.com/my/repo、$ go mod init helloworld,通常我们一般会结合版本控制系统(VCS)实现模块路径的命名
- 然后就可以编写代码,进行编译了,hello.go
package main
import (
"fmt"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Hello())
}
- 编译
$ go build -o hello.go - 执行
$ ./hello $ cat go.mod
module githu.com/my/repo
go 1.14
require rsc.io/quote v1.5.2
5.2 本地包依赖管理
在 Go Modules 没有出来之前,在项目中 import 本地其他包都是通过设置好 GOPATH,将项目路径加入到 GOPATH环境变量中,然后将我们的包放入 $GOPATH/src 下,这样我们就可以找到本地依赖包。比如:
- {your project path}
- bin
- pkg
- src
- {package1 name} # 包名文件夹必须与包名一致
- package files
- {package2 name}
- package files
- main
- main.go
如何用 Go Modules 去实现本地包依赖呢?
在上一节,创建了一个最简单的 Go Modules 项目,我们依赖了 rsc.io/quote 模块,这是一个从公共镜像代理上可获得的模块,但是如果我们自己定了内部的包,这个时候采用 Go Modules 方式如何去找到我们的包呢? 比如 pkg1 和 pkg2:
.
├── bin
├── cmd
│ └── hello
│ └── hello.go
├── go.mod
├── go.sum
├── pkg1
│ ├── pkg1_src.go
│ └── pkg1_test.go
└── pkg2
└── pkg2_src.go
其中 hello.go 、pkg1_src.go 、pkg1_test.go和 pkg2_src.go 内容分别为:
package main
import (
"fmt"
"rsc.io/quote"
"github.com/my/repo/pkg1"
)
func main() {
fmt.Println(quote.Hello())
pkg1.HelloPkg1()
}
package pkg1
import (
"fmt"
)
func HelloPkg1() string {
fmt.Println("Hello pkg1")
return "Hello pkg1"
}
package pkg1
import "testing"
func TestHello(t *testing.T) {
want := "Hello pkg1"
if got := HelloPkg1(); got != want {
t.Errorf("Hello() = %q, want %q", got, want)
}
}
package pkg2
import (
"fmt"
)
func HelloPkg2() {
fmt.Println("Hello pgk2")
}
通过在 hello.go 中使用我们的项目模块路径 + 具体包路径就可以引用到我们需要的本地包了。然后在项目根目录编译,将可执行文件输出到 bin 目录:
# 在项目根目录中编译,./... 模式表示匹配在当前模块中所有的packages
# 注意:采用 ./... -o 只指定目录,不能指定具体的生成对象名称,因为你可能有多个可执行文件一起生成
$ go build -o bin ./...
# 也可以单独编译我们的可执行文件,并指定生成名称
$ go build -o bin/hello_rename cmd/hello/hello.go
在 bin 目录下默认生成 hello 名称的可执行文件,执行 ./bin/hello:
Ahoy, world!
Hello pgk1
你也可以单独将某个包编译成 Go 静态库:
# 单独编译某个包,同样的要找到这个包也需要使用项目模块路径 + 具体包路径
$ go build -buildmode=archive -o bin/libpkg1.a github.com/my/repo/pkg1
5.3 如何发布我们的模块呢?
在完成我们 Go 模块后,如果需要提供给别人使用就需要发布版本,结合版本控制系统(VCS),只要遵循 Go 的语义化版本控制规范,就可以很方便的发布版本:
# 【step1】在发布之前,建议执行 tidy,清除掉无关或者我们使用到但尚未添加进来的模块
$ go mod tidy
# 【step2】测试本项目模块中所有测试样例,确保测试成功,go test all 会测试依赖在内的所有测试样例
$ go test ./...
# 确保 go.sum 和 go.mod 文件都一起提交到该版本中,go.sum 不是类似 nodejs 的 package-local.json 锁文件,更多地它可以帮助校验本地下载地模块是否被篡改
# 【step3】版本提交
# git 提交操作,发布v1.0.0版本
$ git add -A
$ git commit -m "hello: changes for v1.0.0"
$ git tag v1.0.0
$ git push origin v1.0.0
在 Go 版本发布中,模块导入路径默认是省略了 v0、v1 主版本的。至于为什么这样设计,可以参考: https://github.com/golang/go/issues/24301 。
如果要发布 v2 或者更高的版本?在官方的 FAQ 中很详细的介绍了操作和一些建议,比如你有一个版本仓库,已经打上了 v2.0.0 的标记,但是你还没有采用 Go Modules 方式,建议你后续直接打上 v3,从而很清晰的而区分采用了 Go Modules 方式的版本。下面以发布一个 v2+ 版本其中一种方式(另外参见 https://github.com/golang/go/wiki/Modules#releasing-modules-v2-or-higher )作为示例:
# 【step1】 将你的模块路径带上v2+信息,如
$ go mod edit -module github.com/my/repo/v2
# 【step2】 更新你项目中使用了其他本地包的模块路径,都加上v2,如我们上面的hello.go,则变为github.com/my/repo/v2/pkg1
# 【step3】 版本控制发布 v2.x.x tag
5.4 迁移到 Go Modules 包管理
很多 Go 项目使用以前的老的包管理方式,Go 在迁移方面也做了很多工作,包括从以前的依赖管理自动迁移到 Go Modules 方式以及诸多迁移注意事项。这里就不展开了,具体参见 https://github.com/golang/go/wiki/Modules#migrating-to-modules 。
当然,最简单的迁移方式就是使用 Go1.13 或以上版本,重新组织你的项目和依赖,以及所有的导入包路径的修改,这相当于新初始化一个 Go Modules 项目。
6 总结
从最早的 GOPATH 到 Vendor,再到 vgo 的出现, 最终 Go Modules 成熟,Go 的包依赖管理有了一个很大的进步,尤其是版本、资源和模块权限的管理。Go Modules 还有更多的使用细节,这里没有去校验,如果文章中有什么理解错误,欢迎 Gopher 指正。
7 参考资料
注:
golang.google.cn和golang.org可替换,内容一致。golang.google.cn 在中国大陆无需FQ即可访问。
- https://stackoverflow.com/questions/37237036/how-should-i-use-vendor-in-go-1-6
- https://github.com/golang/go/wiki/Modules#how-do-i-use-vendoring-with-modules-is-vendoring-going-away
- https://tip.golang.org/cmd/go/#hdr-Modules_and_vendoring
- https://devopscon.io/blog/go-1-11-new-modules/
- https://developpaper.com/golang-1-5-to-golang-1-12-package-management-golang-vendor-to-go-mod/
- https://github.com/golang/go/wiki/Modules
- https://goproxy.cn/#Usage
- https://github.com/goproxy/goproxy.cn/blob/master/README.zh-CN.md
- https://github.com/golang/go/wiki/Modules#is-gosum-a-lock-file-why-does-gosum-include-information-for-module-versions-i-am-no-longer-using
- https://davidchan0519.github.io/2019/04/05/go-buildmode-c/
- https://github.com/go-modules-by-example/index/blob/master/009_submodules/README.md


浙公网安备 33010602011771号