go module 使用
1 背景
Go 在 1.11 版本上发布了 go module 版本管理方案,用于解决被人诟病已久的 Go 项目版本管理问题。这个方案最早来自 Go & Versioning。这个方案引入了一些新的概念,也抛弃了原有的 GOPATH,导致在使用思路上有一些区别。
2 概念介绍
2.1 开启 go module
go module 目前没有默认开启,开关通过系统环境变量 GO111MODULE
控制。
这个变量有三个可选值
- auto
默认值,如果项目在 $GOPATH 目录内则使用传统的 GOPATH 方式管理,否则使用 Go module 方式管理。
go1.13 后只要项目有 go.mod 文件,即使在 GOPATH 里,也会使用 Go module 方式管理。
- on
不论项目在 $GOPATH 之内还是之外,全部使用 Go module 方式管理。
- off
项目在 $GOPATH 之内使用传统方式管理,不支持项目在 $GOPATH 之外。
2.2 模块(module)
go module 模式引入了一个新的概念 - 模块(module),一个模块是一组包(package)的集合,这些包共用同一个版本信息。
模块里记录了准确的依赖信息。
一个包含 go.mod 的目录及其子目录构成了一个模块 (module);一个目录即是一个包 (package);再加上代码托管平台 (github, gitlab) 的仓库 (repositories),他们的关系如下:
-
一个仓库包含一个或者多个模块
-
一个模块包含一个或多个包
-
一个包包含该目录下的所有 go 源码文件
2.3 go.mod, go.sum
使用 go module 模式进行依赖管理会生成 go.mod, go.sum 两个文件。
go.mod 有四个指令 module
, require
, replace
, exclude
。
常见的 go.mod 文件如下
module github.com/hilaily/test
require (
github.com/gopkg/go-client v0.0.0-20170930090034-2628b1bfb590 // indirect
github.com/gin/ginex v1.3.0
github.com/gopkg/consul v1.0.4
)
replace (
github.com/test/a v0.0.0-20190603160528-e2879d2603b8 => github.com/test/a v0.0.0-20190704135338-bf0b0a45db46
github.com/test/a v0.0.0-20190611023130-5ee11b162b30 => github.com/test/a v0.0.0-20190704135338-bf0b0a45db46
)
module 用来定义当前模块名称
require 是描述依赖信息
replace 用于是用指定依赖(后者)去代替前者。比如 github 的仓库项目替代 golang.org 的。
exclude 用来忽略某些特定版本
2.4 版本
go module 的版本通过仓库的 tag 来确定,所以推荐 master 上的重要版本都打上 tag。
如果是一个仓库有多个模块,可以参考 Is it possible to add a module to a multi-module repository? 使用。
如果没有 tag,go 会使用 v0.0.0-
2.4.1 版本号说明
Go 使用的版本号协议 **semver (Semantic Versioning)****,**它定义的格式是
v<major>.<minor>.<patch>
- v 所有版本号都以 v 开头。
- <major> 主版本号,有大的版本更新,导致 API 和之前版本不兼容。
- <minor> 次版本号,做了向下兼容的新 feature。
- <patch> 修订版本号,做了向下兼容的修复 bug fix。
2.4.2 兼容性原则
如果旧包和新包具有相同的导入路径,则新包必须向后兼容旧包。
基于这个原因,go 将
当主版本号大于等于 v2
时,这个 Module 的 import path 必须在尾部加上 /vN
。
在 go.mod 文件中: module github.com/my/mod/v2
在 require 的时候: `require github.com/my/mod/v2 v2.0.0`
在 import 的时候: import "github.com/my/mod/v2/mypkg"
当主版本号为 v0
或者 v1
时,尾部的 /v0
或 /v1
可以省略。
v0 不需要保证兼容性。v1 升级 v2 的具体操作可以看后面的 FAQ
3 简单使用
3.1 初始化项目
对于一个新的项目,需要执行 go mod init xxxx
初始化 go.mod 文件。xxxx 为模块路径 (比如 github.com/pkg/test),即 import 使用的路径。
然后执行 go mod tidy
整理依赖,这个命令会检索项目添加 go.mod 里面没有的依赖,同时也会删除不再使用的依赖。
3.2 日常操作
3.2.1 添加新依赖
自动添加
在代码里面 import 了某个依赖后,执行 go build
或者 go test
就会自动添加该依赖的最新版到 go.mod。
手动 go get
如果新增了一个依赖,可以使用
go get github.com/pkg/xxx@master
@master 这里也可以指定特定版本,比如 @v1.0.1,或者 @latest 更新到最新版本。也可以指定特定的 commit id,比如 @aabbccdd。
手动编辑 go.mod 文件
在 go.mod 文件里 require 指令下添加依赖信息。格式同下面更新依赖的更改。
3.2.2 更新依赖
使用命令
go get -u github.com/pkg/xxx@v1.0.1
同样可以指定版本,分支,commit id。
在 go.mod 里面修改版本。
比如 go.mod 里的内容如下
require (
github.com/labstack/echo v1.5.6
// 或者 github.com/gin/ginex v0.0.0-20190708083003-59e55ca13873
)
可以手动通过下面三种方式修改版本
// 指定版本
github.com/labstack/echo v1.5.7
// 或者指定更新到最新的版本
github.com/labstack/echo latest
// 指定分支
github.com/labstack/echo master
// 指定 commit id
github.com/labstack/echo 69b028c17642
然后执行 go mod tidy
就会更新依赖并且自动整理 go.mod 文件。
latest 规则为:
最新,打了 tag 标志稳定的版本(非预发行版本),即v1.2.3
如果没有,则选最新,打了tag的预发行版本,即v1.2.3-abc1
如果还没有,则选最新,没有打 tag 的版本,即v0.0.0
。
3.2.3 提交
提交代码时需要提交 go.mod 和 go.sum 两个文件,并且提交前先用 go mod tidy
命令整理一下,避免提交了不再使用的依赖信息。
如果需要把依赖代码放进 vendor 目录则使用 go mod vendor
命令。
3.3 总结
初始化项目使用 go mod init xxxx
日常使用最多的命令就是 go get -u xxxx
来添加依赖和 go mod tidy
来清理依赖。
4 查询依赖原因
4.1 go mod why
go module 支持使用 go mod why 的方式查询该项目为什么需要某个依赖。
比如有个发现项目依赖了 github.com/pkg/test 这个模块,那么可以使用
go mod why -m github.com/pkg/test
就会输出从当前项目到 github.com/pkg/test 这个模块的最短依赖链。
-m 的参数是表示后面的参数是模块,如果后面的参数是包,则不需要 -m 参数。
4.2 go mod graph,go list all
go mod why 命令给出的依赖关系是最短依赖路径,也就是比如项目直接依赖了一个库,同时也有依赖库间接依赖了它。那么是有两条依赖路径的。go mod why 无法显示出来。这时候可以使用类似
go mod graph | grep github.com/pkg/test
的命令查看所有的依赖(后面 FAQ 有详细解释)。
5 Replace 指令
go.mod 文件支持 replace 指令,这个指令可以使用一个指定的模块替换先有模块。
6 FAQ
6.1 SCM 里如何使用 Go module 模式
go1.12
现在 scm 支持 go module 模式编译了,所以使用 go module 的项目可以不添加 vendor 进行编译。
操作如下
-
在 build.sh 里添加
export GO111MODULE=on
表示启用 go module 模式编译。 -
在选择编译镜像的时候选择 go1.12 镜像编译。
go1.13及以上
已经默认配置好对应环境变量,选择 go1.13 镜像无缝使用即可。
6.2 Go 相关系统环境变量怎么配置
// 指定不走 proxy 的包路径,用于公司内网的仓库不走代理
export GOPRIVATE="*.company1.org,*. company1.cn"
// SUMDB 的代理,SUMDB 用于校验版本是否被恶意修改
export GOSUMDB="sum.golang.google.cn"
// GOPROXY,拉取依赖库使用的代理。
// 适用于 GO 版本 1.12
export GOPROXY="https://go-mod-proxy.company1.org,https://goproxy.cn,https://proxy.golang.org"
// 适用于 GO 版本 1.13,1.14
export GOPROXY="https://go-mod-proxy.company1.org,https://goproxy.cn,https://proxy.golang.org,direct"
// 适用于 GO 版本 1.15
export GOPROXY="https://goproxy.company1.org|https://goproxy.cn|direct"
6.3 使用 latest 始终都无法升级到最新版本
首先确实期望升级的版本不是 v2,v3 这样的,如果是这样的,请看后文 6.5 小节 如何升级 V2 版本。
假如项目依赖了 a 模块,现在使用的 v1.0.1,最新的版本是 v1.0.5, 但是每次更新始终无法升级到最新版本。这种情况的原因一般是有间接依赖在 go.mod 里写了 require v1.0.1。
这时候可以使用 go mod graph 确认间接依赖的情况。
go mod graph | grep github.com/pkg/test
github.com/pgk/main github.com/pgk/lib1@v0.0.0-20190827100533-6b180752f72a
github.com/pgk/lib2@v0.0.0-20190825025434-50c639faa7db github.com/pgk/lib1@v0.0.0-20190802105914-74b013135f64
// 上面的输出表示
// main(当前项目)依赖了 lib1,同时 lib2@v0.0.0-20190825025434-50c639faa7db 也依赖了 lib1 的这个版本。
// 而这个 lib2 也是 main 的依赖。
输出结果有两列,前者是一个库,后者是它的依赖。
这种问题可以通过让项目依赖同一个依赖库版本来解决。
另外也可以使用 replace 指令强制使用指定的版本
github.com/pkg/test v0.0.0-20190603160528-e2879d2603b => github.com/pkg/test v2.0.0
6.4 项目如何升级到 V2 版本
- 修改 go.mod 第一行,在
module
那行最后加上/v2
。
module [github.com/pgk/f](http://github.com/mnhkahn/aaa/v2)oo[/v2](http://github.com/mnhkahn/aaa/v2)
- 对于不兼容的改动(除了 v0 和 v1),都必须显示得修改 import 的路径。所以我们的引用需要改成
import "github.com/pkg/foo/v2/config"
-
在所有的地方都需要修改,包括自己的包内和调用方包。可以使用 https://github.com/marwan-at-work/mod 这个工具自动修改。
-
代码提交之后需要打新 tag,v2.0.0。
-
调用方修改引用代码,需要加
v2
,和第二步提到的一样。
go get github.com/pkg/foo/v2
注意 go get github.com/pkg/foo 的形式是拉取不到 v2 的 module 的。
6.5 go.mod 里的 v3.2.1+incompatible 是怎么来的
这种形式表示这个 tag 下的仓库并不是 Go module,只是用 git 打了 tag。
6.6 invalid version: unknown revision xxxxxxx 处理方式
这种情况一般是这个 commit 不见了,比如仓库里执行了 push -f。一般升级版本就可以,如果升级不了可能是间接依赖写死了这个版本号,可以参考前面的 FAQ 使用 replace 解决。
6.7 checksum mismatch 处理方式
这种情况一般是根据 go.mod 拉下来的库和 go.sum 里记录的 sum 值对不上,表示这个版本被做了修改,比如原作者修改代码后重新打了这个 tag。可以去 go.sum 里删除掉对应库的两条记录来解决报错。
6.8 如何排除部分目录
如果项目里有一个目录不需要 go module 接管处理,可以在该目录里添加一个空的 go.mod 文件,那么在项目根目录执行 go tool 命令的时候,go module 模式会排除这个文件夹。
注意
被排除的目录及其子目录里不能有 .go 文件,如果有会被当做是一个正常的模块,那就参考多模块仓库处理
6.9 如何将远端依赖改成本地依赖开发
开发中经常会出现这种情况,自己项目依赖的库 A 也是在开发中,由于 go module 下载的包全部放在 $GOPATH/pkg/mod 目录下,只会拉取线上的最新版本,这样如果需要修改 A 项目的代码及其不方便。这里可以使用 replace 指令在解决。
replace 指令可以使用一个本地目录替换包。
比如我的项目 go.mod 有这样一个依赖
require (
github.com/pkg/test v0.0.0-20190802085650-144f82635c10
)
开发过程中这个库我需要修改代码调试,那么可以在 go.mod 里加上这样一行。
replace github.com/pkg/test v0.0.0-20190802085650-144f82635c10 => /Users/laily/go/src/github.com/pkg/test
这样会使用本地目录 /Users/laily/go/src/github.com/pkg/test
的包来代替这个包,就能方便的辅助开发调试了。
注意,这种方式 => 右侧的目录里必须含有 go.mod 文件,如果没有可以初始化一个。
6.10 cannot find main module; see ‘go help modules’ 错误处理
go get 的操作在 go module 模式下做了一些改变,在 GOPATH 时代,go get -u xxxx 通常被用来安装项目,它本质上相当于 go download + go install,先下载包之后安装。go module 模式后,这个操作是下载包,然后给这个操作所在的项目的 go.mod 里添加上这个依赖。如果当前目录下没有 go.mod 文件,即不是在一个 go module 项目里操作,就会出现这个错误。
6.11 unknow revision vx.x.x
有时拉代码会出现这样的错误
go: code.company1.org/gopkg/consul@v1.1.2: reading code.company1.org/gopkg/consul/go.mod at revision v1.1.2: unknown revision v1.1.2
一种情况是 git 版本过低,更新 git 版本就可以修复。
另一种情况是 go 使用 git http 方式去拉的代码,但是仓库只支持 git ssh 方式。
这种情况通过 go install -x 可以确认
这样就需要配置 gitconfig 把 http 方式转换成 ssh 方式。