你们好,我是一只普通的煎鱼,周四晚上颇有幸邀请到 goproxy.cn 的做者 @盛傲飞(@aofei) 到 Go 夜读给咱们进行第 61 期 《Go Modules、Go Module Proxy 和 goproxy.cn》的技术分享。git
本次 @盛傲飞 的夜读分享,是对 Go Modules 的一次很好的解读,比较贴近工程实践,我必然但愿把这块的知识更多的分享给你们,所以有了今天本篇文章,同时你们也能够多关注 Go 夜读,每周会经过 zoom 在线直播的方式分享 Go 相关的技术话题,但愿对你们有所帮助。github
原文地址:干货满满的 Go Modules 和 goproxy.cngolang
Go 1.11 推出的模块(Modules)为 Go 语言开发者打开了一扇新的大门,理想化的依赖管理解决方案使得 Go 语言朝着计算机编程史上的第一个依赖乌托邦(Deptopia)迈进。随着模块一块儿推出的还有模块代理协议(Module proxy protocol),经过这个协议咱们能够实现 Go 模块代理(Go module proxy),也就是依赖镜像。算法
Go 1.13 的发布为模块带来了大量的改进,因此模块的扶正就是此次 Go 1.13 发布中开发者能直接感受到的最大变化。而问题在于,Go 1.13 中的 GOPROXY 环境变量拥有了一个在中国大陆没法访问到的默认值 proxy.golang.org
,通过你们在 golang/go#31755 中激烈的讨论(有些人甚至将话提上升到了“自由世界”的层次),最终 Go 核心团队仍然没法为中国开发者提供一个可在中国大陆访问的官方模块代理。编程
为了从此中国的 Go 语言开发者能更好地进行开发,七牛云推出了非营利性项目 goproxy.cn
,其目标是为中国和世界上其余地方的 Gopher 们提供一个免费的、可靠的、持续在线的且通过 CDN 加速的模块代理。能够预见将来是属于模块化的,因此 Go 语言开发者能越早切入模块就能越早进入将来。缓存
若是说 Go 1.11 和 Go 1.12 时因为模块的不完善你不肯意切入,那么 Go 1.13 你则能够大胆地开始放心使用。本次分享将讨论如何使用模块和模块代理,以及在它们的使用中会常碰见的坑,还会讲解如何快速搭建本身的私有模块代理,并简单地介绍一下七牛云推出的 goproxy.cn
以及它的出现对于中国 Go 语言开发者来讲重要在何处。安全
使用 Go Modules 时常碰见的坑网络
Go modules (前身 vgo) 是 Go team (Russ Cox) 强推的一个理想化的类语言级依赖管理解决方案,它是和 Go1.11 一同发布的,在 Go1.13 作了大量的优化和调整,目前已经变得比较不错,若是你想用 Go modules,但还停留在 1.11/1.12 版本的话,强烈建议升级。app
首先这并非乱说的,由于 Go modules 确实是被强推出来的,以下:ide
从他强制要求使用语义化版本控制这一点来讲就很理想化了,以下:
这个关键词实际上是我本身瞎编的,我只是单纯地我的认为 Go modules 在设计上就像个语言级特性同样,好比若是你的主版本号发生变动,那么你的代码里的 import path 也得跟着变,它认为主版本号不一样的两个模块版本是彻底不一样的两个模块。此外,Go moduels 在设计上跟 go 整个命令都结合得至关紧密,无处不在,因此我才说它是一个有点儿像语言级的特性,虽然不是太严谨。
那么在上文中提到的 Russ Cox 何许人也呢,不少人应该都知道他,他是 Go 这个项目目前代码提交量最多的人,甚至是第二名的两倍还要多。
Russ Cox 仍是 Go 如今的掌舵人(你们应该知道以前 Go 的掌舵人是 Rob Pike,可是据说因为他本人不喜欢特朗普执政因此离开了美国,而后他岁数也挺大的了,因此也正在逐渐交权,不过如今仍是在参与 Go 的发展)。
Russ Cox 的我的能力至关强,看问题的角度也很独特,这也就是为何他刚一提出 Go modules 的概念就能引发那么大范围的响应。虽然是被强推的,但事实也证实当下的 Go modules 表现得确实很优秀,因此这代表必定程度上的 “独裁” 仍是能够接受的,至少能够保证一个项目能更加专注地朝着一个方向发展。
总之,不管如何 Go modules 如今都成了 Go 语言的一个密不可分的组件。
Go modules 出现的目的之一就是为了解决 GOPATH 的问题,也就至关因而抛弃 GOPATH 了。
Go modules 还处于 Opt-in 阶段,就是你想用就用,不用就不用,不强制你。可是将来颇有可能 Go2 就强制使用了。
有一点须要纠正,就是“模块”和“包”,也就是 “module” 和 “package” 这两个术语并非等价的,是 “集合” 跟 “元素” 的关系,“模块” 包含 “包”,“包” 属于 “模块”,一个 “模块” 是零个、一个或多个 “包” 的集合。
module example.com/foobar go 1.13 require ( example.com/apple v0.1.2 example.com/banana v1.2.3 example.com/banana/v2 v2.3.4 example.com/pineapple v0.0.0-20190924185754-1b0db40df49a ) exclude example.com/banana v1.2.4 replace example.com/apple v0.1.2 => example.com/rda v0.1.0 replace example.com/banana => example.com/hugebanana
go.mod 是启用了 Go moduels 的项目所必须的最重要的文件,它描述了当前项目(也就是当前模块)的元信息,每一行都以一个动词开头,目前有如下 5 个动词:
这里的填写格式基本为包引用路径+版本号,另外比较特殊的是 go $version
,目前从 Go1.13 的代码里来看,还只是个标识做用,暂时未知将来是否有更大的做用。
go.sum 是相似于好比 dep 的 Gopkg.lock 的一类文件,它详细罗列了当前项目直接或间接依赖的全部模块版本,并写明了那些模块版本的 SHA-256 哈希值以备 Go 在从此的操做中保证项目所依赖的那些模块版本不会被篡改。
example.com/apple v0.1.2 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= example.com/apple v0.1.2/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= example.com/banana v1.2.3 h1:qHgHjyoNFV7jgucU8QZUuU4gcdhfs8QW1kw68OD2Lag= example.com/banana v1.2.3/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= example.com/banana/v2 v2.3.4 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI= example.com/banana/v2 v2.3.4/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= ...
咱们能够看到一个模块路径可能有以下两种:
example.com/apple v0.1.2 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= example.com/apple v0.1.2/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
前者为 Go modules 打包整个模块包文件 zip 后再进行 hash 值,然后者为针对 go.mod 的 hash 值。他们二者,要不就是同时存在,要不就是只存在 go.mod hash。
那什么状况下会不存在 zip hash 呢,就是当 Go 认为确定用不到某个模块版本的时候就会省略它的 zip hash,就会出现不存在 zip hash,只存在 go.mod hash 的状况。
这个环境变量主要是 Go modules 的开关,主要有如下参数:
这个环境变量主要是用于设置 Go 模块代理,主要以下:
它的值是一个以英文逗号 “,” 分割的 Go module proxy 列表(稍后讲解)
https://proxy.golang.org,direct
,但很惋惜 proxy.golang.org
在中国没法访问,故而建议使用 goproxy.cn
做为替代,能够执行语句:go env -w GOPROXY=https://goproxy.cn,direct
。刚刚在上面,咱们能够发现值列表中有 “direct” ,它又有什么做用呢。其实值列表中的 “direct” 为特殊指示符,用于指示 Go 回源到模块版本的源地址去抓取(好比 GitHub 等),当值列表中上一个 Go module proxy 返回 404 或 410 错误时,Go 自动尝试列表中的下一个,碰见 “direct” 时回源,碰见 EOF 时终止并抛出相似 “invalid version: unknown revision...” 的错误。
它的值是一个 Go checksum database,用于使 Go 在拉取模块版本时(不管是从源站拉取仍是经过 Go module proxy 拉取)保证拉取到的模块版本数据未经篡改,也能够是“off”即禁止 Go 在后续操做中校验模块版本
<SUMDB_NAME>+<PUBLIC_KEY>
。<SUMDB_NAME>+<PUBLIC_KEY> <SUMDB_URL>
。sum.golang.org
(之因此没有按照上面的格式是由于 Go 对默认值作了特殊处理)。sum.golang.org
在中国没法访问,故而更加建议将 GOPROXY 设置为 goproxy.cn
,由于 goproxy.cn
支持代理 sum.golang.org
。Go checksum database 主要用于保护 Go 不会从任何源头拉到被篡改过的非法 Go 模块版本,其做用(左)和工做机制(右)以下图:
若是有兴趣的小伙伴能够看看 Proposal: Secure the Public Go Module Ecosystem,有详细介绍其算法机制,若是想简单一点,查看 go help module-auth
也是一个不错的选择。
这三个环境变量都是用在当前项目依赖了私有模块,也就是依赖了由 GOPROXY 指定的 Go module proxy 或由 GOSUMDB 指定 Go checksum database 没法访问到的模块时的场景
在使用上来说,好比 GOPRIVATE=*.corp.example.com
表示全部模块路径以 corp.example.com
的下一级域名 (如 team1.corp.example.com
) 为前缀的模块版本都将不通过 Go module proxy 和 Go checksum database,须要注意的是不包括 corp.example.com
自己。
这个主要是针对 Go modules 的全局缓存数听说明,以下:
$GOPATH/pkg/mod
和 $GOPATH/pkg/sum
下,将来或将移至 $GOCACHE/mod
和$GOCACHE/sum
下( 可能会在当 $GOPATH
被淘汰后)。go clean -modcache
清理全部已缓存的模块版本数据。另外在 Go1.11 以后 GOCACHE 已经不容许设置为 off 了,我想着这也是为了模块数据缓存移动位置作准备,所以你们应该尽快作好适配。
第二步: 让 GOPATH 从你的脑海中彻底消失,早一步踏入将来。
go env -w GOBIN=$HOME/bin
。go env -w GO111MODULE=on
。go env -w GOPROXY=https://goproxy.cn,direct
# 在中国是必须的,由于它的默认值被墙了。go mod init <OPTIONAL_MODULE_PATH>
以生成 go.mod 文件。go help module-get
和 go help gopath-get
分别去了解 Go modules 启用和未启用两种状态下的 go get 的行为用 go get
拉取新的依赖
go get golang.org/x/text@latest
master
分支的最新 commit:go get golang.org/x/text@master
go get golang.org/x/text@v0.3.2
go get golang.org/x/text@342b2e
go get -u
更新现有的依赖go mod download
下载 go.mod 文件中指明的全部依赖go mod tidy
整理现有的依赖go mod graph
查看现有的依赖结构go mod init
生成 go.mod 文件 (Go 1.13 中惟一一个能够生成 go.mod 文件的子命令)go mod edit
编辑 go.mod 文件go mod vendor
导出现有的全部依赖 (事实上 Go modules 正在淡化 Vendor 的概念)go mod verify
校验一个模块是否被篡改过这里咱们注意到有两点比较特别,分别是:
go mod vendor
,由于 Go modules 正在淡化 Vendor 的概念,颇有可能 Go2 就去掉了。这里主要是提到 Go1.13 新增了 go env -w
用于写入环境变量,而写入的地方是 os.UserConfigDir
所返回的路径,须要注意的是 go env -w
不会覆写。
这里主要是指从旧有的依赖包管理工具(dep/glide 等)进行迁移时,由于 BUG 的缘由会致使不通过 GOPROXY 的代理,解决方法有以下两个:
这里主要想涉及两块知识点,以下:
在这里再次强调了 Go Module Proxy 的做用(图左),以及其对应的协议交互流程(图右),有兴趣的小伙伴能够认真看一下。
在这块主要介绍了 Goproxy 的实践操做以及 goproxy.cn 的一些 Q&A 和 近况,以下:
Q:若是中国 Go 语言社区没有我们本身家的 Go Module Proxy 会怎么样?
A:在 Go 1.13 中 GOPROXY 和 GOSUMDB 这两个环境变量都有了在中国没法 访问的默认值,尽管我在 golang.org/issue/31755 里努力尝 试过,但最终仍然没法为我们中国的 Go 语言开发者谋得一个完美的解决方案。因此从今之后咱 们中国的全部 Go 语言开发者,只要是 使用了 Go modules 的,那么都必须先修改 GOPROXY 和 GOSUMDB 才能正常使用 Go 作开发,不然可能连一个最简单的程序都跑不起 来(只要它有依 赖第三方模 块)。
Q: 我建立 Goproxy 中国(goproxy.cn)的主要缘由?
A:其实更早的时候,也就是今年年初我也曾 试图在 golang.org/issue/31020 中请求 Go team 能想办法避免那时的 GOPROXY 即将拥有的默认值能够在中国正常访问,但 Go team 彷佛也无能为力,为此我才坚决了建立 goproxy.cn 的信念。既然别人无法儿帮忙,那我们就 得本身动手,不为别的,就为了让你们之后可以更愉快地使用 Go 语言配合 Go modules 作开发。
最初我先是和七牛云的 许叔(七牛云的 创始人兼 CEO 许式伟)提出了我打算 建立 goproxy.cn 的想法,本是抱着 试试看的目的,但没想 到 许叔几乎是没有超过一分钟的考虑便承认了个人想法并表示愿意一块儿推 动。那一阵子恰好遇上我在写毕业论文,因此项目开发完后就 一直没和七牛云作交接,一直跑在个人我的服 务器上。直到有一次 goproxy.cn 被攻击了,一下午的功夫 烧了我一百多美圆,而后我才 意识到这种项目真不能我的来作。我的来作不靠 谱,万一依赖这个项目的人多了,项目再出什么事儿,那就会给你们成没必要要的损 失。因此我赶忙和七牛云作了交接,把 goproxy.cn 彻底交给了七牛云,甚至连域名都过户了去。
此处呈现的是存储大小,主要是针对模块包代码,而通常来说代码并不会有多大,0-10MB,10-50MB 占最大头,也是可以理解,可是大于 100MB 的模块包代码就比较夸张了。
此时主要是展现了一下近期 goproxy.cn 的网络数据状况,我相信将来是会愈来愈高的,值得期待。
Q:如何解决 Go 1.13 在从 GitLab 拉取模块版本时遇到的,Go 错误地按照非指望值的路径寻找目标模块版本结果导致最终目标模块拉取失败的问题?
A:GitLab 中配合 goget 而设置的 <meta>
存在些许问题,致使 Go 1.13 错误地识别了模块的具体路径,这是个 Bug,听说在 GitLab 的新版本中已经被修复了,详细内容能够看 https://github.com/golang/go/... 这个 Issue。而后目前的解决办法的话除了升级 GitLab 的版本外,还能够参考 https://github.com/developer-... 这条回复。
Q:使用 Go modules 时能够同时依赖同一个模块的不一样的两个或者多个小版本(修订版本号不一样)吗?
A:不能够的,Go modules 只能够同时依赖一个模块的不一样的两个或者多个大版本(主版本号不一样)。好比能够同时依赖 example.com/foobar@v1.2.3
和 example.com/foobar/v2@v2.3.4
,由于他们的模块路径(module path)不一样,Go modules 规定主版本号不是 v0 或者 v1 时,那么主版本号必须显式地出如今模块路径的尾部。可是,同时依赖两个或者多个小版本是不支持的。好比若是模块 A 同时直接依赖了模块 B 和模块 C,且模块 A 直接依赖的是模块 C 的 v1.0.0 版本,而后模块 B 直接依赖的是模块 C 的 v1.0.1 版本,那么最终 Go modules 会为模块 A 选用模块 C 的 v1.0.1 版本而不是模块 A 的 go.mod 文件中指明的 v1.0.0 版本。
这是由于 Go modules 认为只要主版本号不变,那么剩下的均可以直接升级采用最新的。可是若是采用了最新的结果致使项目 Break 掉了,那么 Go modules 就会 Fallback 到上一个老的版本,好比在前面的例子中就会 Fallback 到 v1.0.0 版本。
Q:在 go.sum 文件中的一个模块版本的 Hash 校验数据什么状况下会成对出现,什么状况下只会存在一行?
A:一般状况下,在 go.sum 文件中的一个模块版本的 Hash 校验数据会有两行,前一行是该模块的 ZIP 文件的 Hash 校验数据,后一行是该模块的 go.mod 文件的 Hash 校验数据。可是也有些状况下只会出现一行该模块的 go.mod 文件的 Hash 校验数据,而不包含该模块的 ZIP 文件自己的 Hash 校验数据,这个状况发生在 Go modules 断定为你当前这个项目彻底用不到该模块,根本也不会下载该模块的 ZIP 文件,因此就不必对其做出 Hash 校验保证,只须要对该模块的 go.mod 文件做出 Hash 校验保证便可,由于 go.mod 文件是用得着的,在深刻挖取项目依赖的时候要用。
Q:能不能更详细地讲解一下 go.mod 文件中的 replace 动词的行为以及用法?
A:这个 replace 动词的做用是把一个“模块版本”替换为另一个“模块版本”,这是“模块版本”和“模块版本(module path)”之间的替换,“=>”标识符前面的内容是待替换的“模块版本”的“模块路径”,后面的内容是要替换的目标“模块版本”的所在地,即路径,这个路径能够是一个本地磁盘的相对路径,也能够是一个本地磁盘的绝对路径,还能够是一个网络路径,可是这个目标路径并不会在从此你的项目代码中做为你“导入路径(import path)”出现,代码里的“导入路径”仍是得以你替换成的这个目标“模块版本”的“模块路径”做为前缀。
另外须要注意,Go modules 是不支持在 “导入路径” 里写相对路径的。举个例子,若是项目 A 依赖了模块 B,好比模块 B 的“模块路径”是 example.com/b
,而后它在的磁盘路径是 ~/b
,在项目 A 里的 go.mod 文件中你有一行 replace example.com/b=>~/b
,而后在项目 A 里的代码中的“导入路基”就是 import"example.com/b"
,而不是 import"~/b"
,剩下的工做是 Go modules 帮你自动完成了的。
而后就是我在分享中也提到了, exclude 和 replace 这两个动词只做用于当前主模块,也就是当前项目,它所依赖的那些其余模块版本中若是出现了你待替换的那个模块版本的话,Go modules 仍是会为你依赖的那个模块版本去拉取你的这个待替换的模块版本。
举个例子,好比项目 A 直接依赖了模块 B 和模块 C,而后模块 B 也直接依赖了模块 C,那么你在项目 A 中的 go.mod 文件里的 replace c=>~/some/path/c
是只会影响项目 A 里写的代码中,而模块 B 所用到的仍是你 replace 以前的那个 c,并非你替换成的 ~/some/path/c
这个。
在 Go1.13 发布后,接触 Go modules 和 Go module proxy 的人愈来愈多,常常在各类群看到各类小伙伴在咨询,包括我本身也贡献了好几枚 “坑”,所以我以为傲飞的这一次 《Go Modules、Go Module Proxy 和 goproxy.cn》的技术分享,很是的有实践意义。若是后续你们还有什么建议或问题,欢迎随时来讨论。
最后,感谢 goproxy.cn 背后的人们(@七牛云 和 @盛傲飞)对中国 Go 语言社区的无私贡献和奉献。