Go 语言自诞生以来,一路走到今天已经经历了 11 个年头了。其包依赖管理机制也从无到有,从vendor演化成了如今的Go module。Go module 从Go 1.11进入 gopher 们视野,到目前的Go 1.15,其改进和优化一直在持续。在即将到来的Go 1.16中,Go module 将成为默认包依赖管理模式 (即默认 GO111MODULE=on)。但即便如此,我们在进行 go module 的实践过程中依然还会遇到一些 “棘手” 的问题,本文就将针对一个 Go module 实践中的具体问题做深入描述,并告诉你目前可用的最佳解决方案(也许在 go module 的后续演进过程中可能会有更好的解决方案或干脆消除掉这个机制上的问题)。
人总是会犯错的,作为 Go 包/module 的作者,我们偶尔也会出现这样的低级错误:将一个处于 broken 状态的 module 发布了出去。比如:bitbucket.org/bigwhite/m1是我维护的一个 module(专为此文创建的公共 go module),它目前已经进化到 v1.0.1 版本了:
// bitbucket.org/bigwhite/m1/main.go
package m1
import "fmt"
func M1() {
fmt.Println("This is m1.M1 - v1.0.1")
}
m1 这个 module 有两个消费者:c1 和 c2,它们依赖的也都是 m1 的 v1.0.1 版本:
// c1的go.mod
module github.com/bigwhite/c1
go 1.14
require bitbucket.org/bigwhite/m1 v1.0.1
// c1的main.go
package main
import (
"fmt"
"bitbucket.org/bigwhite/m1"
)
func main() {
fmt.Println("This is c1")
m1.M1()
}
// c2的go.mod
module github.com/bigwhite/c2
go 1.14
require bitbucket.org/bigwhite/m1 v1.0.1
// c2的main.go
package main
import (
"fmt"
"bitbucket.org/bigwhite/m1"
)
func main() {
fmt.Println("This is c2")
m1.M1()
}
c1 和 c2 所在的 Go 开发环境均使用下面的 GOPROXY 设置:
export GOPROXY=https://goproxy.cn,direct
我们用一幅示意图来描述当前的状态:
以 c1 为例,构建并运行 c1:
// c1的module root目录下
$go build
go: finding module for package bitbucket.org/bigwhite/m1
go: downloading bitbucket.org/bigwhite/m1 v1.0.1
go: found bitbucket.org/bigwhite/m1 in bitbucket.org/bigwhite/m1 v1.0.1
$./c1
This is c1
This is m1.M1 - v1.0.1
接下来,作为 m1 的作者,我犯了一个低级错误:将更新了的但却无法编译成功的 m1 打标签为 v1.0.2 发布了出去:
// bitbucket.org/bigwhite/m1的m1.go
package m1
import "fmt"
func M1() {
var a int // 编译器错误:a declared but not used
fmt.Println("This is m1.M1 - v1.0.2")
}
// 在m1的module root目录下
$git commit -m"update m1 to v1.0.2(broken)" .
[master af1dd21] update m1 to v1.0.2(broken)
1 file changed, 2 insertions(+), 1 deletion(-)
$git tag -m"tag v1.0.2(broken)" v1.0.2
$git push --tag origin master
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 8 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 492 bytes | 492.00 KiB/s, done.
Total 4 (delta 1), reused 0 (delta 0)
cTo https://bitbucket.org/bigwhite/m1.git
911bbc5..af1dd21 master -> master
* [new tag] v1.0.2 -> v1.0.2
就这样,我一不小心将一个处于 broken 状态的 module 版本 m1@v1.0.2 发布了出去!此时此刻,m1 的 v1.0.2 版本还仅存在于其源仓库站点上,即bitbucket/bigwhite/m1中,在任何一个 GoProxy 服务器上还尚无该版本的缓存。
依赖 m1 的两个项目 c1 和 c2 此时依赖的仍然是 m1@v1.0.1 版本,如未显式升级对 m1 的依赖,c1 和 c2 的构建不会受到处于 broken 状态的 module v1.0.2 版本的影响。
并且此时此刻,由于 m1@v1.0.2 尚未被 GoProxy 服务器所缓存,在 GOPROXY 开启的情况下,go list 是查不到 m1 有可升级的版本的:
// 以c2为例:
$go list -m -u all
github.com/bigwhite/c2
bitbucket.org/bigwhite/m1 v1.0.1
但如若绕开 GOPROXY,那么 go list 则可以查找到 m1 的最新版本为 v1.0.2(我们通过设置 GONOPROXY 来使得 go list 查询 m1 的源仓库而不是代理服务器上的缓存):
$GONOPROXY="bitbucket.org/bigwhite/m1" go list -m -u all
github.com/bigwhite/c2
bitbucket.org/bigwhite/m1 v1.0.1 [v1.0.2]
此时,如若某个 m1 的消费者在 GOPROXY 开启的情况下显式更新对 m1 版本的依赖,以 c2 如此操作为例:
$ go get bitbucket.org/bigwhite/m1@v1.0.2
go: downloading bitbucket.org/bigwhite/m1 v1.0.2
# bitbucket.org/bigwhite/m1
/root/go/pkg/mod/bitbucket.org/bigwhite/m1@v1.0.2/m1.go:6:6: a declared but not used
c2 对 m1 依赖版本的显式更新,触发了 GOPROXY 对 m1@v1.0.2 版本的缓存,上述操作后,当前的状态如下示意图:
这之后,其他 m1 的消费者,比如 c1,便能够在 GOPROXY 开启的情况下查询到 m1 存在新版本 v1.0.2,即使它是 broken 的:
// 以c1为例:
$go list -m -u all
github.com/bigwhite/c1
bitbucket.org/bigwhite/m1 v1.0.1 [v1.0.2]
一旦 broken 的 m1 版本 (v1.0.2) 进入到 Proxy 的缓存,那么其 “危害性” 便 “大肆传播” 开了。此时 module m1 的新消费者都将受到影响!比如这里我们引入一个新的消费者 c3(同样设置 GOPROXY 为 goproxy.cn):
// c3的main.go
package main
import (
"fmt"
"bitbucket.org/bigwhite/m1"
)
func main() {
fmt.Println("This is c3")
m1.M1()
}
c3 的首次构建就会报错:
// c3下:
$go build
go: finding module for package bitbucket.org/bigwhite/m1
go: found bitbucket.org/bigwhite/m1 in bitbucket.org/bigwhite/m1 v1.0.2
# bitbucket.org/bigwhite/m1
/root/go/pkg/mod/bitbucket.org/bigwhite/m1@v1.0.2/m1.go:6:6: a declared but not used
下面是当前问题的最新状态图:
如果在 GOPATH 时代,废掉一个之前发的包版本是分分钟的事情,因为那时包消费者依赖的都是 latest commit。包作者只要 fix 掉问题、提交并重新发布即可。
但是在 go module 时代,作废掉一个已经发布了的 go module 版本,还真不是一件能轻易做好的事情。这很大程度是源于大量 Go module 代理服务器的存在。下面我们来看看可能的问题解决方法:
要解决上述问题,Go 包作者们的一个很直接的解决方法是:重新发布 broken 的 module 版本。但这样做真的能生效么?
如果所有 m1 的消费者都通过 m1 所在代码托管服务器 (bitbucket) 获取 m1 的特定版本,那么这种方法还真能解决掉这个问题。m1 的作者仅需删除掉远程的 tag: v1.0.2,在本地 fix 掉问题,然后重新 tag v1.0.2 并 push 发布到 bitbucket 上的仓库中即可。这样,对于已经 get 到 broken v1.0.2 的消费者来说,只需清除掉本地的 module cache(go clean -modcache),再重新构建即可;对于 m1 的新消费者,直接得到的就是重新发布后的 v1.0.2 版本。
但现实的情况时,Go 在1.13 版本中就将 GOPROXY 的默认值设置为https://proxy.golang.org,direct了,国内我们通常使用七牛云的代理:goproxy.cn。因此,一旦一个 module 版本被发布,当某个消费者通过其配置的 goproxy 获取该版本时,该版本就会在短时间内被缓存在对应的代理服务器上。后续通过该 goproxy 服务器获取那个版本的 m1 时,请求不会再回到 m1 所在的源代码托管服务器,这样即便 m1 的源服务器上的 v1.0.2 版本得到了重新发布,那么散布在各个 goproxy 服务器上的 broken v1.0.2 依旧存在,并且被 “传播” 到各个 m1 消费者的开发环境中,而重新发布后的 v1.0.2 版本却得不到 “传播” 的机会:
因此,从消费者的角度看,m1 的 v1.0.2 版本依旧是一个 broken 的版本,m1 作者的解决措施无效!
很多人问,即便 m1 的作者删除了 v1.0.2 这个发布版本,各大 goproxy 服务器上的 broken v1.0.2 版本是否也会被删除呢?遗憾的告诉你:不会。
Goproxy 服务器当初的一个设计目标就是尽可能的缓存更多包/module。即便某个 module 的源码仓库都被删除了,这个 module 的各个版本依旧缓存在 goproxy 服务器上,这个 module 的消费者依然可以正常获取该 module 并顺利构建。因此,goproxy 服务器当前的实现都没有主动删掉某个 module 缓存的特性。
面对上述问题,Go 社区当前的最佳实践就是发布 module 的新 patch 版本。以上面 m1 为例,我们废除掉 v1.0.2,在本地修正问题后,直接打 v1.0.3 标签,并发布 push 到远程代码服务器上。这样整体状态就变成了下面示意图中样子了:
上述的发布 module 的新 patch 版本的解决方法其实仍存在两个问题:
根据前面的描述,如果尚无消费者手工下载 v1.0.3,那么 proxy server 上不会有 v1.0.3 版本的缓存,在本地通过 go list -u -m all 也查不到 v1.0.3 的存在,除非是在设置 GONOPROXY=bitbucket.org/bigwhite/m1 前提下的 go list 查询。
另外在go 1.15及以前版本中,Go 原生并没有提供标识某个版本作废的机制,在Go 1.16中,module 的作者可以在自己 module 的 go.mod 中使用retract 指示符标识出哪些版本为作废的,不推荐使用的。语法形式如下:
// go.mod
retract v1.0.0 // single version
retract [v1.1.0, v1.2.0] // closed interval
我们还用 m1 为例,我们将 m1 的 go.mod 更新为如下内容:
//m1的go.mod
module bitbucket.org/bigwhite/m1
go 1.16
retract v1.0.2
将其放入 v1.0.3 标签中并发布!现在 m1 的消费者 c2 要查看 m1 是否有最新版本时,可以查看到以下内容 (c2 本地环境使用 go1.16 版本):
$GONOPROXY=bitbucket.org/bigwhite/m1 go list -m -u all
github.com/bigwhite/c5
bitbucket.org/bigwhite/m1 v1.0.2 (retracted) [v1.0.3]
从 go list 的输出结果中,我们看到了 v1.0.2 版本上有了 retracted 的提示,提示该版本已经被 m1 的作者作废了,不应该再使用,应升级为 v1.0.3。但 retracted 仅仅是一个提示作用,并不影响 go build 的结果,c2 环境 (之前在 go.mod 中依赖 m1 的 v1.0.2) 下的 go build 不会自动绕过 v1.0.2,除非显式更新到 v1.0.3。
“Gopher 部落” 知识星球开球了!高品质首发 Go 技术文章,“三天” 首发阅读权,每年两期 Go 语言发展现状分析,每天提前 1 小时阅读到新鲜的 Gopher 日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于 Go 语言生态的所有需求!星球首开,福利自然是少不了的!2020 年年底之前,8.8 折 (很吉利吧^_^) 加入星球,下方图片扫起来吧!
Go 技术专栏 “改善 Go 语⾔编程质量的 50 个有效实践” 正在慕课网火热热销中!本专栏主要满足广大 gopher 关于 Go 语言进阶的需求,围绕如何写出地道且高质量 Go 代码给出 50 条有效实践建议,上线后收到一致好评!78 元简直就是白菜价,简直就是白 piao! 欢迎大家订阅!
我的网课 “Kubernetes 实战:高可用集群搭建、配置、运维与应用” 在慕课网热卖中,欢迎小伙伴们订阅学习!
Gopher Daily(Gopher 每日新闻) 归档仓库 - https://github.com/bigwhite/gopherdaily
我的联系方式: