在前面的 Go 语言模块化编程 - go module ( 二 ) 和 Go 语言模块化编程 - go module ( 三 ) 简单的模块 中我们简单的介绍了下 Go 11 go module 的一些基础的知识
从前面的两个章节我们知道,Go 模块 ( module ) 的目标之一是提供可重用的构建,并通过从存储库中获取正确和预期的文件使这一点做的更好
但是,前面我们所学的关于如何使用 go module 的知识都是在线的。如果服务器离线要怎么做 ? 如果存储库不存在又要怎么做,我们并没有提及。
一般情况下,团队处理这些风险的一种方法是自己组件存储库自己处理依赖关系
但其实有更好的选择,就是随着 Go 1.11 module 一起的 「 模块代理 」 ( module proxy )
Go module 的下载协议
为了更好的让你了解 Go module proxy 的处理机制,我们先从 go module 的下载协议说起,因为只有了解了下载协议,才能更好的知道 go module proxy 的位置和实现
-
当启用 Go 模块支持并且
go
命令确定需要一个模块时,它首先在$GOPATH/pkg/mods
目录下查看本地缓存 -
如果在那个目录下没有找到合适的模块或文件,那么就会继续从网络中获取模块或文件,也就是从
Github
、Gitlab
等上托管的远程仓库
如果我们想要控制哪些模块或文件可以下载,则需要设置环境变量 GOPROXY
为我们的代理 URL
,来告诉 go
获取模块的命令通过我们的代理来下载模块
例如
export GOPROXY=http://gproxy.mycompany.local:8080
这个代理并不复杂,就是一个简简单单的 Web 服务器而且,只是一个可以响应 go module 下载协议的 Web 服务器,顺带提供了一个非常简单的查询和获取模块的 API
如果不提供查询和获取模块的 API,设置可以是一个简简单单的静态文件服务器,比如 Nginx
搭建的。
在添加了代理这个环节下,使用 go
命令获取一个模块,比如 github.com/pkg/errors
的流程一般如下
从这个流程图中我们可以看到
-
go
命令首先询问代理 ( proxy ) ,github.com/pkg/errors
包的所有可用版本这个询问方式就是向代理服务器发起一个
/{module name}/@v/list
的 HTTP 请求 -
然后我们服务器返回一个关于该模块的简单文本格式的版本可用列表
v1.0.1 v1.0.0 v0.8.0 v0.7.1
-
go
命令获取到所有可用版本列表之后,解析并确定需要下载哪个版本。( 一般情况下都是最新版本,除非特别指定 )注意,这两个逻辑很重要,其实第一次发起的查询请求,并没有发送想要的模块版本,而是返回全部版本,这样做的好处就是由客户端自己决定如何做选择,服务器不参与决策
-
go
命令确定好版本之后,比如v1.0.1
,然后再次向代理服务器发起一个/{module name}/@v/{module revision}
的 GET 请求注意,这时候传递了具体版本,因为已经决定要下载哪个版本了
-
我们的代理服务器接到请求后,就返回一个 JSON 格式的响应。
这个响应的
JSON
格式如下{ "Version": "v1.0.1", "Name": "v1.0.1", "Short": "v1.0.1", "Time": "2018-08-27T08:54:46.436183-04:00" }
如果转换为 Go 语言的结构 (
struct
) 格式,则为type RevInfo struct { Version string // version string Name string // complete ID in underlying repository Short string // shortened ID, for use in pseudo-version Time time.Time // commit time }
-
go
命令收到这个 JSON 响应后,相当于再次确认了自己想要的版本存在虽然看起来返回的信息都一样,但这是这个版本的一些元数据
-
随后,
go
命令向代理服务器发起一个/{module name}/@v/{module revision}.mod
的 HTTP GET 请求来获取一个指定版本的go.mod
文件。注意,虽然请求的是
{module revision}.mod
文件,但其实就是每次版本变更提交的go.mod
文件 -
代理服务器再接到请求后就会响应一个文本格式
text/plain
的go.mod
内容,对于我们的范例来说,内容可能就是module github.com/pkg/errors
如果这个模块还依赖了其它模块,那么此文件可能列出其它依赖项,并为每个依赖项重新启动循环
也就是为每一个模块重新启动
1-8
操作步所以呢,其实就是最顶层的依赖最早下载,当前想要的模块最后下载
-
最后,
go
命令会向代理服务器发送一个/{module name}/@v/{module revision}.zip
的 HTTP GET 请求这是一个
zip
压缩包格式,包含了该模块的指定版本的所有文件 -
代理服务器在接收到请求后,就会响应一个二进制的内容,也就是返回一个
application/zip
格式的响应当然了,这个
zip
压缩包里的文件是有严格要求的「 每个文件必须以完整的模块路径和版本为前缀 」
例如
github.com/pkg/errors@v0.8.0/
所以,我们的范例的模块,看起来可能就是下面这样
github.com/pkg/errors@v0.8.0/example_test.go github.com/pkg/errors@v0.8.0/errors_test.go github.com/pkg/errors@v0.8.0/LICENSE ...
而不是这样的
errors/example_test.go errors/errors_test.go errors/LICENSE ...
步骤看起来是不是很复杂? 其实,就是一个非常简单的协议,只需提取 3 或 4 个文件:
- 版本列表(仅当
go
不知道它想要的版本时 ) - 模块的元数据
go.mod
文件内容- 模块所有的文件的
zip
格式的压缩包
结束语
go module 代理协议和流程就是这么简单,因为章节有限,所以,具体实现一个 go module proxy 我们下一章节再来阐述