go modules 是go官方包管理方案。
go 1.14 已经发布,正式推荐 go modules 可用于生产环境。

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/106947/1583056526255-11406b86-8b63-474a-a46e-1c41a469a63d.png#align=left&display=inline&height=539&margin=%5Bobject%20Object%5D&originHeight=539&originWidth=1027&status=done&style=none&width=1027)

今天这篇文章,来介绍 go modules

什么是 go modules

go modules 是 Go 语言的依赖解决方案,发布于 go 1.11,成长为 go 1.12,丰富于 go 1.13,正式于 go1.14 推荐在生产环境上使用。

go modules 目前集成在 go 的工具链中,只要安装了 go, 自然而然就可以使用 go modules 了,而 go modules 的出现也解决了在 go 1.11 前的几个常见争议问题:

  1. go 语言长久以来的依赖管理问题
  2. 淘汰现在的 GOPATH 使用模式
  3. 统一社区中的其他依赖管理工具(提供了迁移功能)

GOPATH

go 1.11 前使用 GOPATH

GOPATH 是什么

使用如下命令查看
go env
```

  1. GO111MODULE="on"
  2. GOARCH="amd64"
  3. GOBIN=""
  4. GOCACHE="/Users/zhangyi/Library/Caches/go-build"
  5. GOENV="/Users/zhangyi/Library/Application Support/go/env"
  6. GOEXE=""
  7. GOFLAGS=""
  8. GOHOSTARCH="amd64"
  9. GOHOSTOS="darwin"
  10. GOINSECURE=""
  11. GOMODCACHE="/Users/zhangyi/go/pkg/mod"
  12. GONOPROXY=""
  13. GONOSUMDB=""
  14. GOOS="darwin"
  15. GOPATH="/Users/zhangyi/go"
  16. GOPRIVATE=""
  17. GOPROXY="https://goproxy.cn,direct"
  18. GOROOT="/usr/local/Cellar/go/1.15.5/libexec"
  19. GOSUMDB="sum.golang.org"
  20. GOTMPDIR=""
  21. GOTOOLDIR="/usr/local/Cellar/go/1.15.5/libexec/pkg/tool/darwin_amd64"
  22. GCCGO="gccgo"
  23. AR="ar"
  24. CC="clang"
  25. CXX="clang++"
  26. CGO_ENABLED="1"
  27. GOMOD="/dev/null"
  28. CGO_CFLAGS="-g -O2"
  29. CGO_CPPFLAGS=""
  30. CGO_CXXFLAGS="-g -O2"
  31. CGO_FFLAGS="-g -O2"
  32. CGO_LDFLAGS="-g -O2"
  33. PKG_CONFIG="pkg-config"
  34. GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/mt/w0md4fbn74xggzrf18zb1v_h0000gn/T/go-build126588908=/tmp/go-build -gno-record-gcc-switches -fno-common"

GOPATH 下包含了三个子目录

  • bin 存放所有编译生成的二进制文件
  • pkg 存放预编译的目标文件,以加快程序的后续编译速度
  • src 存入项目代码
    1. cd $GOPATH
    2. ls
    3. bin pkg
    因此在 GOPATH 模式下,我们需要将应用代码存放在固定的 `<br />go get -u -v 下载到<br />GOPATH` 下。

为什么要弃用 GOPATH

主要原因:

  • GOPATH 模式没有版本控制的概念,具有致命的缺陷,至少会有以下问题
    • 在执行 go get 时,你无法传达任何的版本信息,也就是说你无法知道自己当前更新的哪一个版本,也无法拉取自己所期望的版本
    • 在运行 go 应用时,你无法保证其他人与你所期望依赖的第三方库是相同的版本,也就是说在项目依赖库的管理上,你无法保证所有人的依赖版本一致。
    • 你没办法处理 v1,v2,v3等不同版本的引用问题,因为 GOPATH下导入的路径都是一样的
  • GO语言官方从 go 1.11 起开始推进 go modules

GOPATH 同期其他方案

go 1 在 2012年03月28号发布
go 1.11 是在 2018年08月25号才正式发布
在这个空档时间内,并没有 go modules 这一个东西,早期还好说,因为刚发布,使用的人不多,所以没有明显暴露,但是后期 go 语言使用的人越来越多了,那怎么办?

这时候社区涌现了大量的依赖解决方案,百花齐放;其中有名的有

  • vendor
  • dep

现在已经退出主流。

go modules 使用

在初步了解了 go modules 有历史后,我们正式进入到 go modules 的使用介绍,首先我们将从头开始一个 go modules 项目(原则上所创建的目录应试不要放在 GOPATH 中)

go mod 常用命令

命令 作用
go mod init 生成 go.mod 文件
go mod download 下载 go.mod 中的旨的依赖
go mod tidy 整理现在的依赖
go mod graph 查看现在的依赖结构
go mod edit 编辑 go.mod 文件
go mod vendor 导出项目所有的依赖到 vendor 目录
go mod verify 校验一个模块是否被篡改过
go mod why 查看为什么需要依赖某模块


环境变量

在 go modules 中有如下常用环境变量,我们可以通过 go env 来查看,

  1. GO111MODULE="on"
  2. GOARCH="amd64"
  3. GOBIN=""
  4. GOCACHE="/Users/zhangyi/Library/Caches/go-build"
  5. GOENV="/Users/zhangyi/Library/Application Support/go/env"
  6. GOEXE=""
  7. GOFLAGS=""
  8. GOHOSTARCH="amd64"
  9. GOHOSTOS="darwin"
  10. GOINSECURE=""
  11. GOMODCACHE="/Users/zhangyi/go/pkg/mod"
  12. GONOPROXY=""
  13. GONOSUMDB=""
  14. GOOS="darwin"
  15. GOPATH="/Users/zhangyi/go"
  16. GOPRIVATE=""
  17. GOPROXY="https://goproxy.cn,direct"
  18. GOROOT="/usr/local/Cellar/go/1.15.5/libexec"
  19. GOSUMDB="sum.golang.org"
  20. GOTMPDIR=""
  21. GOTOOLDIR="/usr/local/Cellar/go/1.15.5/libexec/pkg/tool/darwin_amd64"
  22. GCCGO="gccgo"
  23. AR="ar"
  24. CC="clang"
  25. CXX="clang++"
  26. CGO_ENABLED="1"
  27. GOMOD="/dev/null"
  28. CGO_CFLAGS="-g -O2"
  29. CGO_CPPFLAGS=""
  30. CGO_CXXFLAGS="-g -O2"
  31. CGO_FFLAGS="-g -O2"
  32. CGO_LDFLAGS="-g -O2"
  33. PKG_CONFIG="pkg-config"
  34. GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/mt/w0md4fbn74xggzrf18zb1v_h0000gn/T/go-build583788123=/tmp/go-build -gno-record-gcc-switches -fno-common"


每个命令 设置的时候 可以 用go env -w

GO111MODULE

作为 go modules 功能的开关,其允许设置以下参数:

  • auto 只要项目包含了 go.mod 文件就自动启用 go modules
    • 目前在1.11~1.14 中仍然是默认值
  • on 启用 go modules
  • off 禁用 go modules

GOPROXY

用于设置 go 模块代理,作用用于使 go 在后续拉取模块版本时能够脱离传统的 VCS 方式,直接通过镜像来快速拉取。

GOPROXY 的默认值

这有一个严重问题,就是在国内无法访问 https://proxy.golang.org
,因此这会直接卡住你的第一步,所以你必须在开启 go modules 时,指定 GOPROXY

GOPROXY 的值是一个以英文逗号来分割的列表,可以设置多个值;如果你不想使用,也可以设置为 off ,这会禁用 go 在后续操作中使用任何 go 模块代理

direct 是什么?
上面的设置中,列表中有个 direct 标识,它有什么作用呢?
实际上,direct 是一个特殊的标识符,用于指示 go 回源到模块版本的源地址去抓取(比如github等),场景如下:

  1. 当值列表中上一个 go 模块代理返回 404/410 时,
  2. go 自动尝试列表中下一个,
  3. 遇到 direct 时回源(也就是回到源地址去抓取),
  4. 而遇到 EOF 时终止并抛出类似“invalid version: unknown revision…”的错误。

Go Mod 终极入门指南 - 图1

GOSUMDB

用于在拉取模块版本时(无论是从源站还是通过 go modules proxy)保证拉取到的模块的数据未经过篡改,若发布不一致,也就是可能存在篡改

GOSUMDB默认值


同样, 在国内是无法访问的,但是 GOSUMDB 可以这被 GOPROXY 所代理(详见:Proxying a Checksum Database)

因此我们可以通过设置 GOPROXY 来解决,而先前我们所设置的模块代理 goproxy.cn 就能支持代理 sum.golang.org,所以,这一个问题在设置 GOPROXY 后,就不需要关心了

另外若对 GOSUMDB 有自定义需求,其支持中如下格式:

  • 格式1: <SUMDB_NAME>+<PUBLIC_KEY>
  • 格式2: <SUMDB_NAME>+<PUBLIC_KEY> <SUMDB_URL>

同样,也可以设置为 off ,关闭 go 后续操作中校验模块版本功能。

GONOPROXY/GONOSUMDB/GOPRIVATE

针对于私有模块的拉取场景
一般建议直接设置 GORPIVATE,它的值将作为 GONOPROXY/GONOSUMDB 的默认值,所以建议的最佳姿势是直接使用 GORPIVATE

设置的后,前缀为 git.xxx.com 和 github.com/eddycjy/mquote 的模块都会被认为是私有模块。
此处也支持通配符,

这样子设置后,所有要薽路径为 example.com 的子域名都将不经过 GORPOXY 和 GOSUMDB,需要注意的是不包括 example.com 本身。

启用 go mod

目前 go modules 并不是默认开启的,因此 go 语言提供了 GO111MODULE 这个环境变量来作为 go modules 的开关,其允许设置以下参数

  • auto 默认值,只要项目中包含了 go.mod 文件就启用 go moduels
  • on
  • off

如果你不确定你当前的值是什么,可以执行 go env 命令查看
如果需要对 GO111MODULE 的值变更,推荐通过 go env 命令设置

但需要注意的是,如果对应的系统环境变量有值了,go env 是不支持覆盖写入的,否则会出现如下报错信息

初始化项目

在完成 Go modules 的开启后,我们需要创建一个示例项目来进行演示,执行如下命令:

  1. $ mkdir -p $HOME/eddycjy/module-repo
  2. $ cd $HOME/eddycjy/module-repo


初始化

  1. go mod init github.com/eddycjy/module-repo
  2. go: creating new go.mod: module github.com/eddycjy/module-repo




在执行 go mod init 命令时,我们指定了模块导入路径为 github.com/eddycjy/module-repo,接下来我们在该项目根目录下创建 main.go 文件

  1. import (
  2. "fmt"
  3. "github.com/eddycjy/mquote"
  4. )
  5. func main() {
  6. fmt.Println(mquote.GetHello())
  7. }


然后在项目根目录下执行 go get github.com/eddycjy/mquote 命令,如下

  1. go: finding github.com/eddycjy/mquote latest
  2. go: downloading github.com/eddycjy/mquote v0.0.0-20200220041913-e066a990ce6f
  3. go: extracting github.com/eddycjy/mquote v0.0.0-20200220041913-e066a990ce6f


查看 go.mod 文件

在初始化项目时,会自动生成一个 go.mod 文件,是启用了 go modules 项目所必须的最重要的标识,同时也是 GO111MODULE 值为 auto时的识别标识,它描述了当前项目的元信息,每一行都以一个动词开头
在我们刚刚进行了初始化和简单拉取后,我们再次查看 go.mod 文件,基本内容如下:

  1. module github.com/eddycjy/module-repo
  2. go 1.13
  3. require (
  4. github.com/eddycjy/mquote v0.0.0-20200220041913-e066a990ce6f
  5. )


为了更进一步的讲解,我们模拟引用如下

  1. module github.com/eddycjy/module-repo
  2. # 标识当前要模块的 go 语言版本,值为初始化时的版本,目前来看还只是个标识作用。
  3. go 1.13
  4. # 用于设置一个特定的模块版本
  5. require (
  6. example.com/apple v0.1.2
  7. example.com/banana v1.2.3
  8. example.com/banana/v2 v2.3.4
  9. example.com/pear // indirect # indirect 表示该模块为间接模块(可能是你 go get,或者其他模块所依赖的)
  10. example.com/strawberry // incompatible
  11. )
  12. # 用于从使用中排除一个特定的模块版本
  13. exclude example.com/banana v1.2.4
  14. # 用于将一个模块版本替换为另外一个模块版本
  15. replace example.com/apple v0.1.2 => example.com/fried v0.1.0
  16. replace example.com/banana => example.com/fish

查看 go.sum

在第一次拉取模块依赖后,会发现多出了一个 go.sum 文件,其详细罗列了当前项目直接/间接依赖的所有模块的版本,并写明了那些模块版本的 SHA-256 哈希值以备go在售后操作中保证项目所依赖的那些模块版本不会被篡改。

  1. github.com/eddycjy/mquote v0.0.1/go.mod h1:ZtlkDs7Mriynl7wsDQ4cU23okEtVYqHwl7F1eDh4qPg=
  2. github.com/eddycjy/mquote/module/tour v0.0.1 h1:cc+pgV0LnR8Fhou0zNHughT7IbSnLvfUZ+X3fvshrv8=
  3. github.com/eddycjy/mquote/module/tour v0.0.1/go.mod h1:8uL1FOiQJZ4/1hzqQ5mv4Sm7nJcwYu41F3nZmkiWx5I=
  4. ...


我们可以看到一个模块路径可能有如下两种

  1. github.com/eddycjy/mquote v0.0.1/go.mod h1:ZtlkDs7Mriynl7wsDQ4cU23okEtVYqHwl7F1eDh4qPg=


h1 hash 是 go modules 将目标模块版本的 zip 文件开包后,针对所有包内文件依次进行 hash,然后再把它们的 hash 结果按照固定格式和算法组成总的 hash 值。

h1 hash 和 go.mod hash 两者,要不就是同时存在,要不就是只存在 go.mod hash。那么什么情况下不存在 h1 hash 呢? 就是当 go 认为肯定用不到这个模块版本时就会省略它的 h1 hash,就会出现不存在 h1 hash,只存在 go.mod hash 情况。

查看全局缓存

我们刚刚成功的将 github.com/eddycjy/mquote 拉取了下来,其拉取的结果缓存在
GOPATH/pkg/sum 目录下,而在 mod 目录下会以 github.com/foo/bar 的格式进行存放

├── cache
├── github.com
├── golang.org
├── google.golang.org
├── gopkg.in…

需要注意的是同一个模块版本的数据只缓存一份,所有其他模块共享使用。如果你希望清理所有已经缓存的模块,可以使用

go modules 的 go get 行为

在拉取项目依赖时,你会发现拉取的过程总共分了三大步,分别是

  • finding 发现
  • downloading 下载
  • extracting 提取

并且在拉取信息上一共分为了三段内容
截屏2021-01-17 下午1.26.55.png

pkg/mod 目录

  1. ~/go/pkg
  2. ls
  3. mod sumdb
  4. ~/go/pkg
  5. cd mod
  6. go/pkg/mod
  7. ls
  8. cache github.com golang.org gopkg.in
  9. fyne.io go.uber.org google.golang.org moul.io

需要注意的是,所拉取版本的 commit 时间是以 UTC 时区为准,而并非本地时区,同时我们会发现我们 go get 命令所拉取到的版本是 v.0.0.0 ,这是因为我们是直接执行 go get -u 获取的,并没有指定任何的版本信息,由 go modules 自行按内部规则进行选择。

go get 行为

刚刚我们用 go get 命令拉取了新的依赖,那么 go get 又提供了哪些功能有呢? 常用的拉取命令

那么我们想选择具体版本应该如何执行呢?如下

命令 作用
go get golang.org/x/text@latest 拉取最新的版本,若存在 tag,则优先使用。
go get golang.org/x/text@master 拉取 master 分支的最新 commit
go get golang.org/x/text@v0.3.2 拉取 tagv0.3.2 的commit
go get golang/x/text@342b2e 拉取 hash342b2e 的commit,最终会被转换为 v0.3.2





go get 版本选择
我们回顾一下我们拉取的 go get github.com/eddycjy/mquote, 其结果是 v0.0.0-20200220041913-e066a990ce6f ,对照着上面所提到的 go get 行为来看,你可能还会有一些疑惑,
那么就是在 go get 没有指定任何版本的情况下,它的版本选择规则是怎么样的,也就是为什么 go get 拉取的是 v0.0.0,它什么时候会拉取正常带版本号的 tags 呢。实际上这需要区分两种情况,如下

  1. 所拉取的模块有发布 tags:
    1. 如果只有单个模块,那么就取主版本号最大的那个 tag
    2. 如果有多个模块,则推算相应的模块路径,取主版本号最大的那个 tag(子模块的tag的模块路径会有前缀要求)
  2. 所拉取的模块没有发布过 tags
    1. 默认取主分支最新一次 commit 的 commithash

没有发布过 tags

那么为佬以拉取的是 v0.0.0,因为 github.com/eddycjy/mquote 没有发布任何的 tag,

截屏2021-01-17 下午1.28.31.png

因此它默认取的是主分支最新一次 commit 的 commit_time 和 commit_hash,也就是 0200220041913-e066a990ce6f

有发布过 tags

在项目有发布 tags 的情况下,还存在着多种模式,也就是只有单个模块和多个模块,我们统一以多个模块来进行展示,因为多个模块的情况下就已经包含了单个模块的使用了,


截屏2021-01-17 下午1.31.39.png

在这个项目中,我们一共打了两个 tag,分别是 v0.0.1 和 module/tour/v0.0.1. 这时候你可能会奇怪,为什么要打 module/tour/v0.0.1 ,这么奇怪的tag,这有什么意思吗?

其实 go modules 在同一个项目下多个模块的 tag 表现方式,其主要目录结构为

  1. ├── go.mod
  2. ├── module
  3. └── tour
  4. ├── go.mod
  5. └── tour.go
  6. └── quote.go




可以看到 mquote 这个项目的根目录有一个 go.mod 文件,而在 module/tour 目录下也有一个 go.mod 文件,其模块导入和版本信息的对应关系如下

tag 模块导入路径 含义
v0.0.1 github.com/eddycjy/mquote mquote 项目的 v0.0.1版本
module/tour/v0.0.1 github.com/eddycjy/mquote/module/tour mquote 项目下子模块 module/tour 的 v0.0.1 版本



导入主模块和子模块

结合上述内容,拉取主模块的话,还是照旧执行如下命令

go: finding github.com/eddycjy/mquote v0.0.1
go: downloading github.com/eddycjy/mquote v0.0.1
go: extracting github.com/eddycjy/mquote v0.0.1

如果是想拉取子模块,执行

go: finding github.com/eddycjy/mquote/module v0.0.1
go: finding github.com/eddycjy/mquote/module/tour v0.0.1
go: downloading github.com/eddycjy/mquote/module/tour v0.0.1
go: extracting github.com/eddycjy/mquote/module/tour v0.0.1

我们将主模块和子模块的拉取进行对比,你会发现子模块的拉取会多出一步,它会发现 github.com/eddycjy/mquote/module,再继续推算,最终拉取到 module/tour

go mod的导入路径

不同版本的导入路径

在前面的模块拉取和引用中,你会发现我们的模块导入路径就是 github.com/eddycjy/mquote 和 github.com/eddycjy/mquote/module/tour ,似乎并没有什么特殊的。

其实不然,实际上 go modules 在主版本号为 v0 和 v1 的情况下省略了版本号,而在主版本号为 v2及以上刚需要明确指定出主版本号,否则会出现冲突,其 tag 与模块导入路径的大致对应关系如下:

tag 模块导入路径
v0.0.0 github.com/eddycjy/mquote
v1.0.0 github.com/eddycjy/mquote
v2.0.0 github.com/eddycjy/mquote/v2
v3.0.0 github.com/eddycjy/mquote/v3



简单来讲,就是主版本号为 v0 和 v1,不需要在模块导入路径包含主版本信息,而在 v1版本以后,也就是 v2起,必须在模块的导入路径末尾加上主版本号,引用时就需要调整为如下格式

  1. import (
  2. "github.com/eddycjy/mquote/v2/example"
  3. )


另外忽略主版本号 v0 和 v1 是强制性的(不是可选项),因此每个软件包只有一个明确且规范的导入路径。

为什么忽略 v0、v1

  1. 导入路径中忽略 v1 的原因是:考虑到许多开发人员创建一旦到达 v1 版本便永不改变的软件包,这是官方所鼓励的,不认为所有这些开发人员在无意发布 v2版本时都应被迫拥有明确的 v1 版本尾缀,这将导致 v1 版本变成“噪音”且无意义。
  2. 导入路径中忽略了 v0 版本的原因是: 根据语义化版本规范,v0 的这些版本完全没有兼容性保证。需要一个显式的 v0 版本的标识对确保兼容性没有多大帮助。

go modules 语义化版本控制

我们不断地在 go modules 的使用中提到版本号,其实质上被称为“语义化版本”,假设我们的版本号 v1.2.3,如下

截屏2021-01-17 下午1.29.01.png

其版本格式为 主版本号.次版本号.修订号, 版本号的递增规则如下

  1. 主版本号:当你做了不兼容的 API 修改
  2. 次版本号: 当你做了向下兼容的功能性新增
  3. 修订号:当你做了向下兼容的问题修正。

假设你是先行版本号或者特殊情况,可以将版本追加到“主版本.次版本号.修订号”的后面,作为延伸

截屏2021-01-17 下午1.30.19.png

至此我们介绍了 go modules 所支持的两类版本号方式,在我们发布新版本打 tag 时,需要注意遵循,否则不遵循语义化版本规则的版本号都是无法进行拉取的。

go moduels 的最小版本选择

现在我们已经有一个模块,也有发布的 tag,但是一个模块往往依赖着许多其他的模块,并且不同的模块在依赖时很有可能会出现依赖同一个模块的不同版本,如下

截屏2021-01-17 下午1.30.52.png