Go Modules
的一些总结,希望能够更深入了解 Go 最新的包管理方式,以及在实际环境中将它很好的使用起来。
在 Go1.5
之前用 GOPATH
以及 GOROOT
这两个环境变量来管理包的位置,GOROOT
为 Go
的安装目录,以及编译过程中使用到的系统库存放位置,如fmt。Go1.5
到 Go1.7
开始稳定到 Vendor
方式,即依赖包需要放到 $GOPATH/src/vendor
目录下,这样每个项目都有自己的 vendor
目录,但是如果依赖同样的三方包,很容易造成资源重复,Go vendor 出现了几种主流的管理工具,包括 godep
、govendor
、golide
等。
在 Go1.11
之前,GOPATH
是开发时的工作目录,其中包含三个子目录:
go get
下载的包go install
命令将 go build
编译出的二进制可执行文件存放于此在 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 包管理方式了。
Go Modules
是 Go 1.11
推出的功能模块,前身是 vgo
,成长于 Go 1.12
,丰富于 Go 1.13
是 Go 更好的一种模块依赖管理解决方案实现。
而 Go Module Proxy
是随着 Go Modules
一起产生的模块代理协议,通过这个协议,我们可以实现 Go
模块代理,通过镜像网站下载相关依赖模块。
proxy.golang.org
为 Go
官方模块代理网站,不中国用户是无法访问的,而 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
注意:模块可能是一个项目,项目下面可以包含很多包。
Go Modules
是如何实现版本控制的呢?通过强制使用语义化版本控制规范,详见 https://semver.org/lang/zh-CN/
示例,即我们发布版本的时候必须按照官方指定的版本命名格式来发布,具体的:
Tag
没有遵循语义化版本控制规范那么它就会忽略你的 Tag
,然后根据你的 Commit
时间和哈希值再为你生成一个假定的符合语义化版本控制规范的版本号,比如v0.0.1-20180523231146-b3f5c0f6e5f1
。
v
这个字符是必须的Go Modules
默认认为,只要你的主版本号不变,那这个模块版本肯定就不包含重大变更,则我们 import
的时候 path
不会受到影响,比如 v1.0.0
和 v2.0.0
,就是一个重大版本变更,在编写代码 import 模块的时候,v1版本的包名是github.com/xx/xx
,v2版本的包名就是github.com/xx/xx/v2
了,在我们使用go get
的时候也需要带上完整的版本路径才能导入指定的版本。一个模块是通过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
其中:
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
具体的作用就是将一个模块版本替换为另一个模块版本, =>
标志前是待替换版本。
go.sum
文件的作用是为了验证每个下载的模块是否与过去下载的模块匹配,并检测模块是否被恶意篡改。比如你在开发过程中依赖了一个模块的某个版本,完成开发后,你上层版本管理平台时只有go.mod
和go.sum
,如果其他人去使用该项目或者基于该项目开发,则需要在他本地重新下载相应的模块,这时go.sum
里记录的加密校验和就可以校验新环境下下载的模块是否与原始依赖保持一致。
在每一个模块的根目录都有一个go.sum
与go.mod
相匹配,记录go.mod
中每一个依赖模块的加密校验和,校验和的前缀是h
,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
文件中。
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+" # 使用除了sum.golang.org 和 sum.golang.google.cn 域名外其他需要给出公钥
GOSUMDB="sum.golang.org+ 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通配符语法一致,如
*
表示匹配任意长度任意字符串。
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/[email protected]
# 拉取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
$ go mod edit [editing flags] [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]
-v
标志将 tidy 过程中已删除(没有使用到)的模块信息打印到标准错误$ go mod vendor [-v]
-v
标志打印执行命令过程被拷贝的模块和包的名称到标准错误$ go mod why [-m] [-vendor] packages
go mod graph
依赖关系中的一个最短依赖关系,比如 go mod graph
展示出主模块依赖子模块1,子模块1依赖子模块2,则会全部展示,而如果想查某个模块或某些模块依赖了哪些,则可以用 go mod why
。Go 官方 FAQs 上提到我们的项目没有任何模块依赖是否有必要去添加一个 go.mod
文件呢?它的建议是有必要的,这可以让我们不再依赖 GOPATH
环境变量,也有利于模块的生态系统发展和交流,另外也可以作为你项目的一个声明标志,不过一切都是基于在 GO1.11 版本之上。那如何创建一个 Go Modules 项目呢?
Go1.11
版本或以上,建议使用 Go 1.13
版本或以上$ cd
GO111MODULE
环境变量,执行
$ go mod init [your module path]
,如 $ go mod init github.com/my/repo
、 $ go mod init helloworld
,通常我们一般会结合版本控制系统(VCS)实现模块路径的命名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
在 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
在完成我们 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
很多 Go 项目使用以前的老的包管理方式,Go 在迁移方面也做了很多工作,包括从以前的依赖管理自动迁移到 Go Modules 方式以及诸多迁移注意事项。这里就不展开了,具体参见 https://github.com/golang/go/wiki/Modules#migrating-to-modules 。
当然,最简单的迁移方式就是使用 Go1.13 或以上版本,重新组织你的项目和依赖,以及所有的导入包路径的修改,这相当于新初始化一个 Go Modules 项目。
从最早的 GOPATH
到 Vendor
,再到 vgo
的出现, 最终 Go Modules
成熟,Go
的包依赖管理有了一个很大的进步,尤其是版本、资源和模块权限的管理。Go Modules
还有更多的使用细节,这里没有去校验,如果文章中有什么理解错误,欢迎 Gopher
指正。
注:
golang.google.cn
和golang.org
可替换,内容一致。golang.google.cn 在中国大陆无需即可访问。