做者:William Kennedy | 原文:Modules Part 01: Why And Whatgit
最近,我在尝试整理一篇关于 Go 包管理发展历史的文章,但愿能加深本身对这一块知识的认识。在搜集资料的时候,发现了这篇文章,顺手翻译了一下。github
本文是该系列的第一篇,主要介绍包依赖管理中一些基础知识。文中提出了 Go 开发中的三个痛点,如何解决只能在 GOPATH 指定路径开发,如何实现有效的版本管理,以及如何支持 Go 原生工具集依赖管理。针对它们,Go Module 都提供了相应的解决方案。缓存
从第一篇的内容上看,做者后面的文章应该会对 Go 的模块机制进行详细的剖析,很期待。话说,总感受这篇文章翻译的有点别扭,检查的时候发现有好几处语义理解错误,尴尬。bash
翻译正文以下:服务器
Go Module 是 Go 为包依赖管理提供的一个综合性解决方案。从 Go 第一版发布以来,Go 开发者针对包管理这一块提出过三个痛点问题。ide
如何实如今 GOPATH 工做区以外进行代码开发;工具
如何实现依赖版本化管理和有效识别出使用依赖的兼容性问题;测试
如何实现经过 Go 原生工具进行依赖管理;fetch
随着 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 团队如何设计了这个方案打下了基础。接下来还有一些问题须要讨论,好比:
在接下来的文章中,我计划将针对这些问题提供一个更深度的理解。如今,你要确保本身已经明白了仓库、包和模块之间的关系。