作者:William Kennedy | 原文:Modules Part 01: Why And What
最近,我在尝试整理一篇关于 Go 包管理发展历史的文章,希望能加深自己对这一块知识的认识。在搜集资料的时候,发现了这篇文章,顺手翻译了一下。
本文是该系列的第一篇,主要介绍包依赖管理中一些基础知识。文中提出了 Go 开发中的三个痛点,如何解决只能在 GOPATH 指定路径开发,如何实现有效的版本管理,以及如何支持 Go 原生工具集依赖管理。针对它们,Go Module 都提供了相应的解决方案。
从第一篇的内容上看,作者后面的文章应该会对 Go 的模块机制进行详细的剖析,很期待。话说,总感觉这篇文章翻译的有点别扭,检查的时候发现有好几处语义理解错误,尴尬。
翻译正文如下:
Go Module 是 Go 为包依赖管理提供的一个综合性解决方案。从 Go 初版发布以来,Go 开发者针对包管理这一块提出过三个痛点问题。
如何实现在 GOPATH 工作区之外进行代码开发;
如何实现依赖版本化管理和有效识别出使用依赖的兼容性问题;
如何实现通过 Go 原生工具进行依赖管理;
随着 Go 1.13 的发布,这三个问题都得到了解决。在过去的两年里,Go 团队成员为此付出了巨大的努力。本文中将重点介绍从 GOPATH 到模块机制的变化,还有模块究竟解决了什么问题。我将通过足够易懂的语言向大家说明模块的工作机制。
我觉得,重点要理解为什么模块这样工作。
GOPATH 是用于指定 Go 工作区的物理位置,一直以来都很好地服务着 Go 的开发者们。但它对非 Go 开发者并不友好,想在没有任何配置的情况下,随时随地进行 Go 开发,这是不可能的一件事。
Go 团队要解决的第一个问题就是允许 Go 的源码仓库能被 clone 在磁盘中的任意位置,而不仅仅是 GOPATH 指定的工作区。并且 Go 工具集仍然要能成功定位、编译构建与测试它们。
上图展示了一个 github 仓库,ardanlabs/conf
,这个仓库仅有一个包,它用于提供对应用配置处理的支持。
以前,如果想使用这个包,我们需要通过 go get
并指定仓库的规范化名称实现下载一份到你的 GOPATH 下。仓库规范化的名称是由远程仓库的基础 url 和仓库名称两部分组成。
一个例子,在 Go Module 之前,如果你执行 go get github.com/ardanlabs/conf
,代码将会被 clone 到 $GOPATH/src/github.com/ardanlabs/conf
目录下。基于 GOPATH 和仓库名,无论我们把工作区设置何处,Go 工具集始终都能正确地找到代码的位置。
清单 1
package conf_test
import (
...
"github.com/ardanlabs/conf"
...
)
清单1 显示了 conf
包中测试文件 conf_test.go
中的导入其他包的代码片段。
当测试包名用 _test
命名,这就意味着测试代码和被测试代码是在不同的包中,测试代码必须导入要被测试的外部代码。从上面的代码片段中,我们可以看出,测试代码是如何将 conf 导入的。基于 GOPATH 机制,可以非常容易地解析出导入包的路径。然后,Go 工具集就可以成功定位、编译和测试代码。
如果 GOPATH 不存在或者目录结构与仓库名称不匹配,将会如何呢?
清单 2
import "github.com/ardanlabs/conf"
// GOPATH mode: Physical location on disk matches the GOPATH
// and Canonical name of the repo.
// GOPATH 模式:磁盘物理位置与 GOPATH 和仓库的规范名称相匹配
$GOPATH/src/github.com/ardanlabs/conf
// Module mode: Physical location on disk doesn’t represent
// the Canonical name of the repo.
// Module 模式:磁盘上的物理位置和仓库全名没有必然的匹配关系。
/users/bill/conf
清单2 展示了如果把仓库 clone 到任意位置将会产生什么问题。当开发者选择将代码下载他们希望的任意位置时,通过 import 包名称解析出源码的实际位置就不行了。
如何解决这个问题?
我们可以指定一个特殊的文件,使用它指定仓库的规范名称。这个文件的位置可理解为是 GOPATH 的一个替代,在它其中定义了仓库的规范名称,Go 工具可以通过这个名称解析源码中导入包的位置,而不必关心仓库被 clone 到了什么地方。
我们把这个特殊的文件命名为 go.mod
,将在这个文件中定义的由规范名称表示的新实体称为 Module。
清单 3
module github.com/ardanlabs/conf
清单3 中显示了 conf
仓库中的 go.mod
文件的第一行 。
这一行定义了模块的名称,它同时也代表了仓库全名,开发者期待使用它来引用库中任意部分的代码。现在,库被下载到什么位置已经不再那么重要了,Go 工具集会根据 module 文件所在位置和模块名定位和解析内部包的导入,比如前面的示例中,在测试文件中的导入 conf
包。
现在,模块机制允许我们将代码下载到任意位置。那下一个要解决的问题就是如何将代码捆绑到一起进行版本控制。
多数的版本管理系统都支持了在任意提交点打标签。这些标签通常是被用来发布新特性(v1.0.0、v2.3.8,等等),而且一般都是不可变的。
图中显示,conf
已经被打了三个不同的版本标签。这三个标签遵循着语义化版本的格式。
利用版本管理工具,我们可以通过指定 tag 实现 clone 任意版本的 conf
包的目的。但这有两个问题亟待解决。
一旦回答完这两个问题,又会产生第三个问题:
接着,情况变得更差。
为了要使用特定版本的 conf
包,你必须要下载 conf
的所有依赖。对于所有存在依赖传递的项目,这是一个共性的问题。
在 GOPATH 模式下,可以使用 go get
识别和下载所有的依赖包,然后放到 GOPATH 指定的工作区下。但这不是一个完美的方案,因为 go get
仅仅只能从 master
分支下载和更新最新的代码。当初期写代码时,从 master
下载代码没什么问题。但几个月后,有些依赖可能已经升级了,master
分支的最新代码可能已经不再兼容你的项目。这是因为你的项目没有遵守明确的版本管理,任何的升级都可能带来一个不兼容的改变。
在 Module 模式下,通过 go get
下载所有的依赖到一个单一的工作区不再是首选方式。你需要一种方式实现为整个项目中的每个依赖指定一个兼容版本。同时,还要支持针对同一个依赖不同主版本的引入,以防止出现一个项目中依赖同一个包的不同主版本。
针对上面的这些问题,社区已经开发了一些解决方案,如 dep, godep, glide 等。但 Go 需要一个集成的解决方案。这个方案通过重用 go.mod 文件实现按版本维护这些直接和间接依赖。然后,将任何一个版本的依赖当成一个不可变的代码包。这个特定版本不可变的代码包被称为一个 Module。
上图显示了仓库和模块的关系。它显示了如何引用到一个特定版本模块中的包。在这种情况下,在 conf-1.1.0
的代码从版本为 0.3.1
的 go-cmp
导入了 cmp
包。既然,依赖信息已经在 conf
模块中(保存在模块文件中),Go 就可以通过内置的工具集获取指定版本的模块进行编译构建。
一旦有了模块,许多便利的工程体验就体现了出来:
在这方面是非常值得庆幸地,因为在 Go 1.13 中,Go 团队已经提供了许多这方面的支持。
这篇文章尝试为后面讨论 Go 模块是什么以及 Go 团队如何设计了这个方案打下了基础。接下来还有一些问题需要讨论,比如:
在接下来的文章中,我计划将针对这些问题提供一个更深度的理解。现在,你要确保自己已经明白了仓库、包和模块之间的关系。