Go工程化(七) Go Module

本系列为 Go 进阶训练营 笔记,预计 2021Q2 完成更新,访问 博客: Go进阶训练营, 即可查看当前更新进度,部分文章篇幅较长,使用 PC 大屏浏览体验更佳。

3 月进度: 04/15 3 月开始会尝试爆更模式,争取做到两天更新一篇文章,如果感兴趣可以拉到文章最下方获取关注方式。

本文将会分为两部分,第一部分会简单介绍一下 go module 的使用,算是一个简明教程,第二部分会重点介绍一下使用 go module 使用过程当中会遇到的一些坑的解决办法。

PS: Go 的版本管理一直是一个槽点,虽然 go module 已经解决了很多问题,但是槽点还是比较多,本文会介绍一些常见的坑,如果遇到了其他的坑也不要慌,Google 可以帮助你

Go Module 简明教程

从 go 1.11 的初步支持,到 1.16 后的默认开启,go module 已经经历了 5 个版本,已然成为了创建 go 项目的首选包管理方式,这一趴我们就先看一下 go module 的基本使用。

使用 Go Module

初始化

1
2
# go mod init [包路径]
go mod init github.com/mohuishou/go-mod-example

执行上述命令会在当前目录下生成一个 go.mod 文件

1
2
3
module github.com/mohuishou/go-mod-example

go 1.16

添加依赖

执行 go get 包 添加添加对应库到 go mod 中

1
go get github.com/sirupsen/logrus

这时候会在 go mod 中添加如下信息

1
2
3
4
5
module github.com/mohuishou/go-mod-example

go 1.16

require github.com/sirupsen/logrus v1.8.0 // indirect
  • 在 go get 的时候如果不手动指定版本信息,会自动拉取最新的版本的包
  • 如果想要拉取指定版本可以通过 go get github.com/sirupsen/logrus@v1.7.0 的方式,支持
    • @版本号 例如 `@v1.7.0`
    • @分支名 例如 @master
    • @commit tag 例如 @6cff360233dc4457f1536e4f3df4e4e740fd3410
  • // indirect 表示,我们在代码中没有直接应用这个包

执行完 go get 命令之后还会在目录下生成一个 go.sum 文件

1
2
3
4
5
6
7
8
......

github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU=
github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A=

......

这个文件主要包含当前依赖的所有的包,像 go-difflib 我们没有直接依赖,但是我们依赖的 logrus 有依赖它,所以会列在这里

h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 这一串是一个加密的哈希数据,用来保证这个版本一定是一致的,避免包的发布者删除了这个版本之后,修改代码重复发布

现在我们在代码中使用这个包试试

1
2
3
4
5
6
7
package main

import "github.com/sirupsen/logrus"

func main() {
logrus.Info("hello")
}

清理依赖

随着我们开发,可能会有一些包,之前依赖但是后面就不再依赖了,这个时候我们如果要清理哪些不再需要的依赖可以执行下面的命令来进行清理

1
go mod tidy

发布 Go Module

Go 版本号

go 默认使用语义化的版本来表示版本号,基本的方式

  • vMAJOR.MINOR.PATCH
    • 有破坏性变更的时候需要增加主版本号,也就是 MAJOR ,例如 v1.0.0 -> v2.0.0
    • 当有新增的函数或者是 API 时,我们增加 MINOR 版本号,例如 v1.1.0 -> v1.2.0
    • 当没有新的 feature 的时候,例如 bug 修复时,我们增加 PATCH 版本号,例如 v1.1.1 -> v1.1.2
  • 除此之外我们还可以在版本号后面使用 - 表示一些特殊的预发布版本例如 alpha beta 版本等
    • v1.1.1-alpha
    • v1.1.1-beta.2
    • 上面这种特殊的版本只会在手动指定的版本号的时候才会去获取它,默认情况或者是在更新版本的时候不会获取这些版本
  • 为了兼容在 Go Module 出现之前的一些版本,你可能会看到还有一种比较特殊的版本号
    • v0.0.0-20170915032832-14c0d48ead0c
    • 如果依赖的仓库从来没有发布过版本,就会以这种方式存在

v1 及之前版本的发布

1.0 之前的版本发布非常简单,只需要做两件事情

  • 添加一个 LICENSE 文件(非必须)
  • 使用 git tag v1.0.0 加一个版本 tag 即可

v2 及之后版本的发布

2.0 之后的版本发布就麻烦一些了,因为 Go Module 的限制,v2 之后的版本需要在 go.mod 中显示的指定 /v2 主版本好标识,用户在使用导入包的时候也必须加上这个版本标志,这个好处就是可以同时同时导入不同版本的包,但是在升级的时候就比较蛋疼了,必须要将所有文件的导入路径都修改一下

那么该如何发布新版本的包呢?官方推荐的操作是

  • 我们先在当前目录下创建一个 v2 文件夹,应为这样可以兼容那些还在使用 GOPATH 的用户,当然这不是必须的
  • 然后我们再修改一下 go.mod 文件, 在包名后加上主版本号,例如
    • module github.com/mohuishou/go-mod-example/v2
  • 最后我们再使用 git tag v2.0.0 打一个版本并发布即可

Go Module 避坑指南

1. 拉取依赖很慢,有的包还拉取不到

Go 默认的 GOPROXY 配置是 proxy.golang.org , 默认国内无法访问,我们可以配置国内镜像,推荐 goproxy.cn 或者 goproxy.io

1
go env -w GOPROXY=https://goproxy.cn,direct

1.16 之前 Go Module 并未默认开启还需要配置

1
go env -w GO111MODULE=on

2. 公司私有仓库包如何获取

Go 获取包的时候默认会走 PROXY,这个只要你们的库没有对公网发布,那就获取不到,可以通过设置环境变量解决

1
go env -w GOPRIVATE=gitlab.com/xxx

3. 依赖的包被自动升级

在 1.16 后, go build, go test, go get 等命令已经不会自动升级我们依赖的包了,但是在 1.16 之前,这个操作很难受。

首先,这个操作非常的反直觉,其次还有可能会导致非预期的 bug,虽然在 Go Module 的设计当中,主版本不变的情况下都应该保持向前兼容,但是很多知名的第三库都做不到这个,包括 Google 自己开发的 grpc,我们之前就出现过由于 grpc 版本自动升级导致的程序连接错误,必须要回退版本才行。

三个解决办法都可以解决:

  • 升级 Go 版本到 1.16
  • 使用 -mod=readonly ,例如 go build -mod=readonly
  • go.mod 文件中使用 replace 指令指定版本

4. 包的源代码仓库删库了怎么办?

这个其实在 Go Module 上还好一些,因为 Go Module 默认使用 Go Proxy 只要你使用的库的 LICENSE 和 GOPROXY 没有问题,一般都会有缓存

  • 建议公司需要搭建一套自己的 GOPROXY
  • 建议使用官方的 GOPROXY 或者是 goproxy.cn

5. logrus 包名问题

1
2
3
4
5
go: downloading github.com/Sirupsen/logrus v1.4.1
go get: github.com/Sirupsen/logrus@v1.0.6 updating to
github.com/Sirupsen/logrus@v1.4.1: parsing go.mod:
module declares its path as: github.com/sirupsen/logrus
but was required as: github.com/Sirupsen/logrus

现在这个错误应该比较少了,但是我们碰到过很多次了,主要的原因就是 logrus 的作者改了 github 名字,从 Sirupsen -> sirupsen 就导致了大量依赖 logrus 库的第三方库报错冲突,这个的解决方案也是使用 replace

go.mod 的最后加上这么一句就可以了

1
2
3
replace (
github.com/Sirupsen/logrus v1.4.1 => github.com/sirupsen/logrus v1.4.1
)

6. go.sum git merge 冲突

其实很多包管理都有类似的问题,解决方法一般情况下 git merge 合并后再重新执行 go mod tidy 清理一下即可

7. Go 版本升级无法使用新特性

举个例子,我从 go 1.15 升级到 1.16 想使用 embed,也就是静态文件打包的特性,但是我们发现升级 Go 之后执行还是会报错

1
2
3
❯ go run ./main.go
# command-line-arguments
./main.go:9:3: go:embed requires go1.16 or later (-lang was set to go1.15; check go.mod)

这种情况修改一下 go.mod 文件中 go 1.15go 1.16 即可

总结

关于 Go Module 的介绍就到这里了,说实话 Go 的包管理一直以来都是社区的痛点,从 GOPATH 到 Go Vendor 再到 depglide 等社区工具再到现在的 go mod 整体来说还是变得越来越好,特别是最新的 go 1.16 版本,如果没有其他问题的话还是建议升级的,仅默认 readonly 模式这一项就可以少很多事情

参考文献

  1. Go 进阶训练营-极客时间
  2. https://blog.golang.org/modules2019
  3. https://blog.golang.org/using-go-modules
  4. https://blog.golang.org/migrating-to-go-modules
  5. https://blog.golang.org/module-mirror-launch
  6. https://blog.golang.org/publishing-go-modules
  7. https://blog.golang.org/v2-go-modules
  8. https://blog.golang.org/module-compatibility
  9. Go mod 七宗罪
  10. golang 1.13 - 依赖管理从 dep 到 mod 踩坑 | 存档 Save&Load
  11. Go 填坑之将 Private 仓库用作 module 依赖 | 小木屋

第 0 期已经结束,想要报名后面课程的同学,我联系极客时间为大家争取到了读者专属优惠
扫描下方微信公众号二维码,发送【福利】获取专属优惠,比官方优惠更给力哦

关注我获取更新

wechat
知乎
开发者头条
github