本文译自 Defining Go Modules, Go & Versioning 的第 6 部分, 版权@归原文所有.
正如在概述文章中介绍的, Go 模块是作为一个版本化的软件包集合单元, 连同列出其他所需模块的 go.mod 文件.
转向模块是我们重新审视和修正 go 命令如何管理源代码的许多细节的机会.
在我们打算使用模块弃用当前的 go get
模型时, 10 年已经过去了.
我们需要确保模块设计将在未来十年为我们提供良好的服务. 尤其是:
- 我们希望鼓励更多的开发人员为他们的软件包打标签(tag), 而不是期望用户只会选择一个对他们来说看起来很好的提交哈希. 标记显式发布清楚地表明了对别人有用的以及正在开发的东西. 同时, 它仍然可以 - 尽管可能不方便 - 请求特定的提交.
- 我们想脱离版本控制工具如 bzr, fossil, git, hg, 以及 svn 来下载源代码. 因为这些破坏了生态系统: 例如, 使用 Bazaar 或 Fossil 开发的软件包对于不能或不选择安装这些工具的用户而言是不可用的. 版本控制工具也是激动人心的 安全 问题的来源. 将它们移到安全边界之外是一件好事.
- 我们希望允许在单个源代码库中开发多个模块, 但版本保持独立.
尽管大多数开发人员可能会继续使用每个代码库作为一个模块, 但大型项目可能会因在一个代码库中拥有多个模块而受益.
例如, 我们希望将
golang.org/x/text
保留为单个代码库, 但能够将从既定软件包分离出去的实验性新软件包分别版本化. - 我们希望让个人和公司能够轻松地将缓存代理放在
go get
之前进行下载, 无论是否可用(使用本地副本以确保明天下载依然工作)或是否安全(包在公司使用之前进行验证). - 我们希望在将来的某个时候, 为 Go 社区引入一个共享代理, 类似于 Rust, Node 和其他语言所使用的代理. 同时, 设计必须在没有假设代理或注册的情况下运行良好.
- 我们希望消除 vendor 目录. 它们是为了可重复性和可用性而推出的, 但我们现在拥有更好的机制. 可重现性由正确的版本控制处理, 可用性由缓存代理处理.
这篇文章介绍了解决这些问题的 vgo 设计的部分内容. 这里的一切都是初步的: 如果我们发现它不正确, 我们会改变设计.
版本化发布
抽象界限让项目规模化. 最初, 所有的 Go 软件包都可以被所有其他的 Go 软件包导入. 我们在 Go 1.4 中引入了内部目录约定, 以消除开发人员选择构造一个包含多个软件包的程序的问题, 因为需要担心其他用户导入内部辅助软件包, 这些辅助软件包并不公用.
Go 社区现在与代码库提交有类似的可见性问题. 今天, 用户通过提交标识符(通常是 Git 哈希) 来识别包版本是非常普遍的, 其结果是, 将工作组织为一系列提交的开发人员需要担心, 至少在他们的想法背后, 用户固定的这些提交可能并不为公开使用. (译注: 作者的意思是说对于 Git 这种来说某一个 commit 有可能只是实验性质的提交, 本质上说不是为了公用.) 我们需要改变 Go 开源社区的期许, 以确立作者标签发布和用户所喜欢的规范.
我不认同这一点: 用户应该从作者发布的版本中进行选择, 而不是从 Git 历史记录中挑选单独的提交, 这一点尤其有争议. 困难的部分正在改变规范. 我们需要让作者易于标记提交并方便用户使用这些标记.
作者今天共享代码的最常用方式是代码托管网站, 尤其是 GitHub. 对于 GitHub 上的代码, 所有作者需要做的是标记提交并推送标签.
我们还计划提供一个工具(可能称为 go release
)来比较不同版本的模块在类型级别的 API 兼容性, 以捕获类型系统中可见的意外破坏性更改, 以及帮助作者在发布的时候做出决定是次要版本(因为它增加了新的 API 或更改了许多行代码)还是只是一个补丁版本.
对于用户来说, vgo 本身完全是根据标签版本来运作的. 然而, 我们知道, 至少在从旧实践向新实践过渡的过程中, 也许无限期地作为引导新项目的一种方式, 为了允许指定提交, 必须有一个逃生舱口. (译注: 原文作者戏虐的说法, 其实就是变通方案.) 这在 vgo 中是可能的, 但它的设计是为了让用户更倾向于明确标记的版本.
具体来说, vgo 理解特殊的伪版本 v0.0.0-yyyymmddhhmmss-commit
引用给定的提交标识符, 它通常是一个缩短的 Git 哈希, 并且必须具有与(UTC)时间戳匹配的提交时间.
这种形式是 v0.0.0
预发布的有效语义版本字符串. 例如, 节选自 Gopkg.toml 的一段:
[[projects]]
name = "google.golang.org/appengine"
packages = [
"internal",
"internal/base",
"internal/datastore",
"internal/log",
"internal/remote_api",
"internal/urlfetch",
"urlfetch"
]
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
version = "v1.0.0"
[[projects]]
branch = "master"
name = "github.com/google/go-github"
packages = ["github"]
revision = "922ceac0585d40f97d283d921f872fc50480e06e"
对应于这些 go.mod 行:
require (
"google.golang.org/appengine" v1.0.0
"github.com/google/go-github" v0.0.0-20180116225909-922ceac0585d
)
选择伪版本形式以便标准的 semver 优先级规则按提交时间比较两个伪版本, 因为时间戳编码使字符串比较匹配时间比较.
这种形式还可以确保 vgo 始终优先使用带标签的语义版本而不使用未标记的伪版本, 即使 v0.0.1 非常旧, 它的优先级比任何 v0.0.0 预发布版本都要高. (还要注意, 这与dep向项目添加新依赖项时所做的选择相匹配.)
当然, 伪版本字符串很难使用: 它们在 go.mod 文件里, 也可以 vgo list -m
输出.
所有这些不便之处都有助于鼓励作者和用户更喜欢明确标记的版本, 有点像不得不写 import "unsafe"
这种额外的步骤一样鼓励开发人员倾向于编写安全的代码.
go.mod 文件
模块版本由源文件树定义. go.mod 文件描述了该模块, 并且还指出了根目录. 当 vgo 运行在一个目录中时, 它会查看当前目录, 然后查找连续父目录, 以找到标记根目录的 go.mod.
文件格式是面向行的, 只通过 "//" 注释. 每行保存单个指令, 该指令是单个动词(module, require, exclude, 或者 replace, 由最小版本选择所定义), 随后是参数:
module "my/thing"
require "other/thing" v1.0.2
require "new/thing" v2.3.4
exclude "old/thing" v1.2.3
replace "bad/thing" v1.4.5 => "good/thing" v1.4.5
顶头的动词可以从临近的行里分解出来, 自成一个块, 就像 Go 导入包一样:
require (
"new/thing" v2.3.4
"old/thing" v1.2.3
)
我对文件格式的目标是 (1) 清晰和简单, (2) 易于人们阅读, 编辑, 操作和比较, (3) 易于 vgo 等程序读取, 修改和回写, 保留评论和总体结构, 以及 (4) 有限的未来增长空间. 我看了 JSON, TOML, XML 和 YAML, 但他们没有一个似乎同时拥有这四个属性. 例如, Gopkg.toml 上面使用的方法为每个依赖书写三行, 这使得它们更难以浏览, 排序和比较. 相反, 我设计了一个最小格式, 让人联想到 Go程序的头部, 但希望没有足够接近让人困惑. 我改写了一个已存在的注释友好的解析器.
最终集成的 go 命令可能会更改文件格式, 甚至采用更标准的框架, 但对于兼容性, 我们将保持继续阅读今天 go.mod 文件的能力, 就如同 vgo 还可以从 GLOCKFILE, Godeps/Godeps.json
, Gopkg.lock
, dependencies.tsv
, glide.lock
, vendor.conf
, vendor.yml
, vendor/manifest
, 以及 vendor/vendor.json
文件中读取依赖信息一样.
从代码库到模块
开发人员在版本控制系统中工作, 显然 vgo 必须尽可能简单. 例如, 期望开发人员自己准备模块存档(archives)是不合理的. 相反, vgo 可以按照一些基本的, 不显眼的约定, 从任何版本控制库直接导出模块.
首先, 创建一个代码库并用类似于 v0.1.0 这样的一个 semver 格式标记一个提交, 就足够了. 顶头的 v 是必需的, 并且还需要三个数字. 虽然 vgo 它自己接受命令行中 v0.1 的简写形式, 但规范形式 v0.1.0 必须在代码库标记中使用, 以避免歧义. 只有标签是必需的. 为了不使用 vgo 的时候也可以使用提交, 在这一点上一个 go.mod 文件要求并不严格. 创建新的标记提交可创建新的模块版本. 简单.
当开发人员到达 v2 时, 语义导入版本控制意味着 /v2/
在模块根前缀的末尾添加了一个导入路径: my/thing/v2/sub/pkg
.
正如前面的文章中所述, 这个约束有很好的理由, 但是它仍然偏离现有的工具.
意识到这一点, vgo 如果不先检查 go.mod 文件中有主版本的模块路径声明(例如, module "my/thing/v2"
) 就不会使用源码库中的 v2 或更高标记版本.
Vgo 使用该声明作为作者使用语义导入版本控制来命名该模块中的包的证据.
这对多包模块来说尤其重要, 因为模块中的导入路径必须包含 /v2/
元素以避免引用回 v1 模块.
我们预计大多数开发人员会更喜欢遵循通常的 "主分支" 约定, 其中不同的主版本存在于不同的分支中. 在这种情况下, v2 分支中的根目录将有一个表明 v2 的 go.mod, 如下所示:
这大致是大多数开发人员已在工作的(流程).
在图中, v1.0.0 标签指向一个早于 vgo 的提交. 它根本没有 go.mod 文件, 而且工作正常.
在提交标记 v1.0.1 中, 作者添加了一个 go.mod module "my/thing"
.
然而, 在那个提交之后, 作者分叉了一个新的 v2 开发分支.
除了任何更改代码提示 V2 (包括更换 bar 用 quux)时, go.mod 在新的分支更新为 module "my/thing/v2"
. 分支可以独立前进.
事实上, vgo 真的不知道分支(的存在),. 它只是将标签解析为提交, 然后在提交中查看 go.mod 文件.
同样, 该 go.mod 文件是 v2 及更高版本所必需的, 以便 vgo 可以使用该文件中的 module 行作为代码使用了语义导入版本控制的标志, 所以 foo 导入的是 my/thing/v2/foo/quux
, 不是 my/thing/foo/quux
.
作为替代方案, vgo 还支持 "主子目录" 约定, 在子目录中开发了 V1 以上的主要版本:
在这种情况下, v2.0.0 不是通过将整个树分叉为单独的分支而是通过将其复制到子目录中来创建的. 再次 go.mod 更新为 "my/thing/v2"
.
之后, v1.x.x 标记指向根目录文件的提交, 不包含 v2/
, 而 v2.x.x 标记仅指向 v2/
子目录的提交.
go.mod 文件可以使 vgo 区分这两种情况. 将 v1.x.x 和 v2.x.x 标记指向相同的提交也是有意义的: 它们将处理提交的不同子树.
我们期望开发者可能会强烈地选择一种或另一种约定. vgo 同时支持这两种. 请注意, 对于 v2 以上的主版本, 主子目录方案可能会为 go get
用户提供优雅的过渡.
另一方面, dep 或 vendoring 工具的用户应该能够使用任何约定之一来使用代码库. 当然, 我们会确保 dep 可以.
多模块代码库
开发人员也可能发现在单个源代码库中维护一组模块是很有用的. 我们想让 vgo 支持这种可能性. 总的来说, 不同的开发人员, 团队, 项目和公司应用源代码控制的方式已经有很大差异, 我们认为将 "单一代码库等同于一个模块" 这样的单一映射强加给所有开发人员并不是很有成效. 在这方面有一定的灵活性也应该有助于 vgo 适应围绕源码控制的最佳实践不断变化.
在主子目录约定中, v2/
包含模块 "my/thing/v2"
.
一个自然的扩展允许没有为主版本命名的子目录.
例如, 我们可以添加一个 blue/
包含 "my/thing/blue"
模块的子目录, 并由具有该模块路径 blue/go.mod
的文件确认.
在这种情况下, 处理该模块的源代码控制提交标签将采用这种形式 blue/v1.x.x
.
同样, 标签 blue/v2.x.x
将处理 blue/v2/
子目录. blue/go.mod
文件的存在将 blue/
树从外部 my/thing
模块中排除.
在 Go 项目中, 我们打算探索使用这个约定来允许像 golang.org/x/text
这样的代码库定义多个独立的模块.
这让我们保留了粗粒度源码控制的便利, 但仍然在不同的时间将不同的子树提升到 v1.
已过时的版本
作者还需要能够弃用一个版本, 以表明它不应该再被使用.
这还没有在 vgo 原型中实现, 但它可以工作的一种方式是在代码托管站点上定义一个标记 v1.0.0+deprecated
(理想情况下指向与 v1.0.0 相同的提交)将表明该提交已被弃用.
当然重要的是不要完全删除标签, 因为这会破坏构建.
弃用的模块会以某种方式在 vgo list -m -u
输出中高亮显示("显示我的模块和有关更新的信息"), 以便用户知道要更新.
另外, 因为程序可以在运行时访问自己的模块列表和版本, 所以程序也可以配置为根据某些选定的权限检查自己的模块版本, 并在运行弃用版本时以某种方式自行报告. 同样, 这里的细节还没有解决, 但是一旦开发人员和工具共享描述版本的词汇表, 这就是一个很好的例子.
发布
给定一个源代码控制库, 开发人员需要能够以 vgo 可以使用的形式发布它.
在一般情况下, 我们将提供一个命令, 作者运行它们将其源代码控制代码库转换为通过任何静态文件 web 服务器提供给 vgo 使用的文件树.
与当前 go get
类似, vgo 需要一个带有 <meta>
标签的页面来帮助将模块名称转换为该模块的文件树.
例如, 要查找 swtch.com/testmod
, vgo 命令将这样获取通常的页面:
$ curl -sSL 'https://swtch.com/testmod?go-get=1'
<!DOCTYPE html>
<meta name="go-import" content="swtch.com/testmod mod https://storage.googleapis.com/gomodules/rsc">
Nothing to see here.
$
mod 服务器类型表明模块通过该基本 URL 上的文件数提供. storage.googleapis.com/gomodules/rsc
中这个简单案例中的相关文件是:
- .../swtch.com/testmod/@v/list
- .../swtch.com/testmod/@v/v1.0.0.info
- .../swtch.com/testmod/@v/v1.0.0.mod
- .../swtch.com/testmod/@v/v1.0.0.zip
这些 URL 的确切含义在后面的 "下载协议" 一节中讨论.
代码托管网站
对于代码托管网站上巨大数量的开发, 我们希望 vgo 尽可能顺利地融入进来.
而不希望开发人员在其他地方发布模块, 然后让 vgo 支持使用基于 HTTP 的API 直接从这些站点读取所需信息.
一般来说, 档案下载可以比现有版本控制签出快得多.
例如, 在使用千兆互联网连接的笔记本电脑上工作时, 需要 10 秒钟将 CockroachDB 源码树作为 GitHub 的 zip 文件下载, 但需要大约 4 分钟的时间进行 git clone
.
网站只需提供一个可以通过简单的 HTTP GET
获取的任何形式的存档.
例如, Gerrit 服务器仅支持下载 gzipped
归档文件. Vgo 将下载的档案转换为标准形式.
最初的原型只包括对 GitHub 和 Go 项目的 Gerrit 服务器的支持, 但是在进入 Go 主工具链之前, 我们也会增加对 Bitbucket 和其他主要托管站点的支持.
通过轻量级代码库约定(主要与开发人员已经在做的事情相匹配)以及对已知代码托管站点的支持相结合, 我们预计大多数开源活动都不会受到向模块转移的影响,而只是简单地在每个代码库里面添加 go.mod.
使用旧版本 go get
(直接使用 git 和其他源码控制工具)的公司需要进行调整.
也许编写一个满足 vgo 期望但使用版本控制工具的代理是有意义的. 然后, 公司可以运行其中的一种产生类似使用开源托管站点的体验.
模块档案
从代码库到模块的映射有点复杂, 因为开发人员使用源代码控制的方式各不相同.
最终目标是将所有复杂性映射到代理或其他代码使用者(例如 godoc.org
或任何代码检查工具)使用的 Go 模块的通用单一格式.
vgo 原型中的标准格式是 zip 档案, 其中所有路径都以模块路径和版本开头. 例如, 运行 rsc.io/quoteV1.5.2
的 vgo get
后, 你可以在 vgo 的下载缓存里找到 zip 文件:
$ unzip -l $GOPATH/src/v/cache/rsc.io/quote/@v/v1.5.2.zip
1479 00-00-1980 00:00 rsc.io/[email protected]/LICENSE
131 00-00-1980 00:00 rsc.io/[email protected]/README.md
240 00-00-1980 00:00 rsc.io/[email protected]/buggy/buggy_test.go
55 00-00-1980 00:00 rsc.io/[email protected]/go.mod
793 00-00-1980 00:00 rsc.io/[email protected]/quote.go
917 00-00-1980 00:00 rsc.io/[email protected]/quote_test.go
$
我使用了 zip, 因为它是精心指定的, 广泛支持的, 并且如果需要可以干净地扩展, 并允许随机访问单个文件. (相比之下, tar 文件是另一个显而易见的选择, 但是都不满足这些特点.)
下载协议
要下载有关模块的信息以及模块本身, vgo原型仅发出简单的 HTTP GET
请求. 一个关键的设计目标是使得可以从静态托管站点提供模块, 因此请求没有 URL 查询参数.
正如我们前面看到的, 自定义域可以指定模块托管在特定的基本 URL. 像 vgo 今天实现的那样(但是, vgo 的所有这些可能会发生变化), 该模块托管服务器必须提供四种请求格式:*
GET baseURL/module/@v/list
获取所有已知版本的列表, 每行一个.GET baseURL/module/@v/version.info
获取有关该版本的 JSON 格式的元数据.GET baseURL/module/@v/version.mod
获取该版本的 go.mod 文件.GET baseURL/module/@v/version.zip
获取该版本的 zip 文件.
以 version.info
形式提供的 JSON 信息可能会演化, 但现在它对应于此结构:
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
}
vgo list -m -u
命令通过使用 Time 字段显示每个可用更新的提交时间.
一个通用的模块托管服务器也可以有选择地响应非 semver 版本的 version.info
请求. 像这样的 vgo 命令:
vgo get my/thing/[email protected]
将获取 1459def.info
并使用 Time 和 Short 字段派生伪版本.
还有两种可选的请求形式:
GET baseURL/module/@t/yyyymmddhhmmss
在给定的时间戳之前返回或返回最新版本的.info
JSON.GET baseURL/module/@t/yyyymmddhhmmss/branch
同上, 但将搜索限制在给定分支上的提交.
这些支持在 vgo 使用未标记的提交. 如果 vgo 正在添加一个模块却根本找不到标签提交, 它将使用第一种形式来查找截至目前的最新提交.
它在查找可用更新时也是如此, 假定仍然没有标记提交. 分支限制形式用于 gopkg.in
的内部模拟. 这些形式也支持命令行语法:
vgo get my/thing/[email protected]:34:45
vgo get my/thing/[email protected]:34:[email protected]
这可能是一个失误, 但他们在今天的原型, 所以我提到他们.
代理服务器
个人和公司都可能更喜欢从代理服务器下载 Go 模块, 无论是为了效率, 可用性, 安全性, 许可证合规性还是任何其他原因.
如前两节所述, 使用标准的 Go 模块格式和标准的下载协议使得引入对代理的支持变得微不足道.
如果 $GOPROXY 设置了环境变量, vgo 则从给定的基础 URL 获取服务器中的所有模块, 而不是从其通常的位置获取. 为了便于调试, $GOPROXY
甚至可以是指向本地文件树的 file:/// URL.
我们打算编写一个基于 vgo 本地缓存的基本代理服务器, 根据需要下载新模块. 在一组计算机中共享这样的代理将有助于减少来自代理用户的冗余下载, 但更重要的是确保将来的可用性, 即使原始副本消失. 代理也可以选择不允许下载新模块. 在此模式下, 代理会将可用模块限制到代理管理员列入白名单的那些模块. 这两种代理模式都是企业环境中经常需要的功能.
也许有一天, 建立 go get
默认使用的分布式代理服务器集合是有意义, 以确保全球 Go 开发者的模块可用性和快速下载. 但还没准备好. 今天, 我们专注于确保 go get
无需假设任何类型的集中式代理服务器即可运行.
Vendoring 的结束
Vendor 目录有两个目的. 首先, 他们通过其内容指定在 go build
过程中使用的依赖关系的确切版本. 其次, 即使原始副本消失, 它们也可确保这些依赖项的可用性.
另一方面,vendor 目录也很难管理和膨胀它们出现的代码库. 通过 go.mod 文件指定要在 vgo build
使用的依赖关系的确切版本, 以及代理服务器确保可用性, vendor 目录现在几乎完全是冗余的.
但是, 它们可以为最终目的服务: 实现向新版本世界的平稳过渡.
在构建模块时 vgo (和稍后 go) 将完全忽略 vendored 的依赖; 这些依赖也不会包含在模块的 zip 文件中.
为了能够让作者迁移到 vgo 与 go.mod, 同时仍然支持没有完成转换的用户, 新的 vgo vendor
命令通过填充模块的 vendor 目录中用户使用的包来产生基于 vgo 的构建.
接下来呢 ?
这里的细节可能会被修改, 但今天的 go.mod 文件将被任何未来的工具所理解. 请开始使用发布标签标记你的软件包; 请添加 go.mod 文件使你的项目有意义.
本系列的下一篇文章将介绍对 go 工具命令行体验的更改.