即将到来的Go1.13会将module转正,是时候学习它了

01简介

和1.12引入了对modules(模块)的初步支持,这是一个能让依赖项的版本信息更加明确和易于管理的依赖管理系统。本文旨在为你使用模块提供基本的操作指导。后续会有一篇文章来说明如何发布一个模块以供别人使用。

一个模块是一系列Go代码包的集合,它们保存在同一个文件树中。文件树的根目录中包含了一个文件。文件定义了一个模块的modulepath,这就是模块根目录的导入路径。文件还定义了模块的depencyrequirements(依赖项要求),即为了编译本模块,需要用到哪些其它的模块。每一项依赖项要求都包含了依赖项的modulepath,还要指定它的语义版本号。

对于来说,当你工作目录不在$GOPATH/src里面,并且工作目录或者工作目录的任意级父目录中含有文件,go命令行工具会启用模块机制。(但为了兼容以前的机制,当你工作目录是在$GOPATH/src里面的时候,go命令行工具不会启用模块机制,即使工作目录里面有文件也一样。详情请见go命令工具文档)。从开始,模块机制在所有情况下都将会默认启用。

本文将覆盖Go开发中涉及到模块的一系列常用操作:

创建一个新的模块

添加一个依赖项

升级依赖项

添加一个拥有更高主版本号的依赖项

更新当前依赖项到一个新的主版本号

移除无用的依赖项

02创建一个新的模块

让我们来创建一个新的模块吧。

在$GOPATH/src之外创建一个新的、空白的目录,进入这个目录,并且新建一个源代码文件,:

packagehellofuncHello()string{return"Hello,world."}

让我们再写点测试,在hello_文件中:

packagehelloimport"testing"funcTestHello(t*){want:="Hello,world."ifgot:=Hello();got!=want{("Hello()=%q,want%q",got,want)}}

到目前为止,目录里面包含了一个代码包,但是它还不是一个模块,因为这里面没有文件。如果我们现在的工作目录是/home/gopher/hello并且我们运行gotest,我们会看到如下的输出:

$gotestPASSok_/home/gopher/$

最后一行总结了代码包整体的测试结果,因为我们工作目录在$GOPATH之外,且又不属于任何模块,所以Go命令行工具并不知道当前目录的导入路径是什么,所以只能用目录的路径作为包的名字:_/home/gopher/hello。

让我们把当前的目录设置成模块的根目录吧,为此我们要用到gomodinit命令然后再尝试运行gotest:

$/hellogo::/hello$/$

恭喜你,你编写并测试了你的第一个模块!

gomodinit命令创建了一个文件:

$/$

文件只存在于模块的根目录中。模块子目录的代码包的导入路径等于模块根目录的导入路径(就是前面说的modulepath)加上子目录的相对路径。比如,我们如果创建了一个子目录叫world,我们不需要(也不会想要)在子目录里面再运行一次gomodinit了,这个代码包会被认为就是/hello模块的一部分,而这个代码包的导入路径就是/hello/world。

03添加依赖项

引进Go模块系统的主要动机,就是让用户更轻松地使用其他开发者编写的代码(换句话说就是添加一个依赖项)。

让我们修改一下我们的,让它导入/quote模块,并用这个模块的接口来实现Hello:

packagehelloimport"/quote"funcHello()string{()}

现在让我们再运行一遍测试:

$gotestgo:/:/:/:/:/x/:/:/:/x/:/x//$

go命令行工具会根据里面指定好的依赖的模块版本来下载相应的依赖模块。在你的代码中import了一个包,但文件里面又没有指定这个包的时候,go命令行工具会自动寻找包含这个代码包的模块的最新版本,并添加到中(这里的"最新"指的是:它是最近一次被tag的稳定版本(即非预发布版本,non-prerelease),如果没有,则是最近一次被tag的预发布版本,如果没有,则是最新的没有被tag过的版本)。在我们的例子是,gotest把新导入的/quote包解析为/模块。它还会下载/quote模块依赖的两个依赖项。即/sampler和/x/text。但是只有直接依赖会记录在文件里面:

$//$

第二次运行gotest命令的时候Go命令工具就不再重复上述的工作了,因为已经是更新过了,并且刚才下载下来的模块已经缓存在本地(在$GOPATH/pkg/mod)目录中:

$/$

要注意的是虽然用Go命令行工具添加依赖非常的简单快捷,但是它不是没有代价的。你的项目现在有很多关键指标都对新的依赖项有了依赖,比方说代码的正确性、安全性、合适的版权等等,不一而足。更多相关的资讯,可以查看RussCox的博客文章,"OurSoftwareDepencyProblem"。

正如我们上面所见,添加一个直接依赖往往会带来其它间接的依赖。golist-mall命令会把当前的模块和它所有的依赖项都列出来:

$//x///$

在上述golist命令的输出中,当前的模块,又称为主模块(mainmodule),永远都在第一行,接着是主模块的依赖项,以依赖项的modulepath排序。

/x/text的版本是一个典型的伪版本(pseudo-version)的例子,它其实就是Go命令工具自定义的一个命名规则,当你想要依赖一个模块的某个commit版本的代码,但是这个commit没有被tag过的时候,可以这样子来指定。

除了之外,go命令行工具还维护了一个文件,它包含了指定的模块的版本内容的哈希值作为校验参考:

$/x/:/x//:/:w5fcysjrx7yqtD/aO+//:/:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh///:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9$

go命令行工具使用文件来确保你的项目依赖的模块不会发生变化——无论是恶意的,还是意外的,或者是其它的什么原因。文件和文件都应该保存到你的代码版本控制系统里面去。

04更新依赖项

有了Go模块机制后,模块的版本通过带有语义化版本号(semanticversion)的标签来指定。一个语义化版本号包括三个部分:主版本号(major)、次版本号(minor)、修订号(patch)。举个例子:对于版本,主版本号是0,次版本号是1,修订号是2。我们先来过一遍更新某个模块的次版本号的流程。在下一节,我们再考虑主版本号的更新。

从golist-mall的输出中,我们可以看到我们在使用的/x/text模块还是以前没有被打过版本号标签的版本。让我们来把它更新到最新的有打过版本号标签的的版本,并测试是否能正常使用。

$/x/textgo:/x/:/x/:/x/$/$

哇嗷,一切正常!我们再来看看现在golist-mall的输出和文件长什么样子:

$//x///$/(/x////)$

/x/text模块已经被升级到最新的版本(),文件里面也把这个模块的版本指定成版本。注释indirect意味着这个依赖项不是直接被当前模块使用的。而是被模块的其它依赖项使用的。详情请见gohelpmodules。

现在让我们来尝试更新/sampler模块的次版本号。同样操作,先运行goget命令,然后跑一遍测试:

$/samplergo:/:/:/$gotest---FAIL:TestHello(0.00s)hello_:8:Hello()="99bottlesofbeeronthewall,99bottlesofbeer,",want"Hello,world."/$

噢,糟糕,测试报错了,这个测试表明/sampler模块的最新版本跟我们之前的用法不兼容。我们来列举一下这个模块能用的tag过的版本:

$//$

我们之前用过,而明显不能用了。也许我们能试一下版本

$/sampler@:/:/:/$/$

请注意我们给goget命令的参数后面显式地指定了@,事实上每个传递给goget的参数都能在后面显式地指定一个版本号,默认情况下这个版本号是@latest,这代表Go命令行工具会尝试下载最新的版本。

05添加一个拥有更高主版本号的依赖项

我们来为我们的代码包添加一个新的函数,funcProverb会返回一条关于Go并发编程的名言,这可以通过调用来实现,而这个函数由/quote/v3模块提供。首先我们要修改我们的来添加新的函数:

packagehelloimport("/quote"quoteV3"/quote/v3")funcHello()string{()}funcProverb()string{()}

然后我们在hello_里面添加一个测试:

funcTestProverb(t*){want:="Concurrencyisnotparallelism."ifgot:=Proverb();got!=want{("Proverb()=%q,want%q",got,want)}}

然后我们可以来测试我们的代码了:

$gotestgo:/quote/:/quote/:/quote//$

请注意我们的模块现在既依赖/quote也依赖/quote/v3:

$///quote/$

不同主版本号的同一个Go模块,使用了不同的modulepath——从v2开始,modulepath的结尾一定要跟上主要版本号。在本例中,v3版本的/quote已经不是/quote了,它的modulepath是/quote/v3。这个规定被称为semanticimportversioning(语义化的导入版本控制),它给予了不兼容的模块一个不同的名字。反之,版本的/quote应该做到能够向下兼容版本。所以这两个版本可以共用同一个名字/quote。(在上一节中,版本的/sampler应该要向下兼容版本的/sampler,但是bug或者模块使用者对模块的用法不对,这些都有可能会导致错误发生。)

每一次构建项目,go命令行工具允许每个modulepath最多只有一个,这就意味着每一个主要版本只有一个:最多一个/quote,最多一个/quote/v2,最多只有一个/quote/v3等等。这给了模块的作者一个明确的信号:对于同一个modulepath,可能会存在有多个重复的模块——一个程序有可能同时使用了的/quote和的/quote。还有,允许同一个模块的不同主版本号存在(因为它们的modulepath不一样),能够给模块使用者部分更新主版本的能力。拿这个例子来说,我们想要使用/quote/模块里面的,但是没准备好要整个代码都迁移到v3版本的/quote中,这种部分迁移的能力,在大型程序或者代码库里面尤其重要。

06更新当前依赖项到一个新的主版本号

现在让我们把整个项目的/quote都升级到/quote/v3吧。因为主版本号改变了,所以我们应该做好心理准备,可能会有些API已经被移除、重命名或者被修改成了不兼容的方式。通过阅读文档,我们得知Hello已经变成了HelloV3:

$/quote/v3packagequote//import"/quote"()stringfuncGlassV3()stringfuncGoV3()stringfuncHelloV3()stringfuncOptV3()string$

(上面的输出有个已知的Bug:显示的import路径后面漏了v3)

我们可以把中使用()的地方改成()

packagehelloimportquoteV3"/quote/v3"funcHello()string{()}funcProverb()string{()}

然后我们再重新运行一下测试确保一切正常:

$/
07移除没有用到的依赖

我们代码中已经没有用到/quote的地方了,但是它还是会存在golist-mall的输出和文件中:

$//x///quote//$/(/x/////quote////indirect)$

为什么会这样?因为我们在构建一个代码包的时候(比如说gobuild或者gotest),可以轻易的知道哪些依赖缺失,从而将它自动添加进来,但很难知道哪些依赖可以被安全的移除掉。移除一个依赖项需要在检查完模块中所有代码包和这些代码包的所有可能的编译标签的组合。一个普通的build命令不会获得这么多的信息,所以它不能保证安全地移除掉没用的依赖项。

可以用gomodtidy命令来清除这些没用到的依赖项:

$gomodtidy$//x//quote//$/(/x////quote////indirect)$/$
08结论

Go的模块功能将会成为未来Go的依赖管理系统。在所有支持模块机制的Go版本中(即目前的、),都能正常使用模块系统拥有的所有功能。

本文介绍了使用Go模块过程中的几个工作流程:

gomodinit创建了一个新的模块,初始化文件并且生成相应的描述

gobuild,gotest和其它构建代码包的命令,会在需要的时候在文件中添加新的依赖项

golist-mall列出了当前模块所有的依赖项

goget修改指定依赖项的版本(或者添加一个新的依赖项)

gomodtidy移除模块中没有用到的依赖项。

via:

作者:TylerBui-Palsulich,EnoCompton译者:Alex-liutao校对:polaris1119

本文由GCTT原创编译,Go语言中文网荣誉推出

本文由GCTT原创翻译,Go语言中文网首发。也想加入译者行列,为开源做一些自己的贡献么?欢迎加入GCTT!

翻译工作和译文发表仅用于学习和交流目的,翻译工作遵照CC-BY-NC-SA协议规定,如果我们的工作有侵犯到您的权益,请及时联系我们。

欢迎遵照CC-BY-NC-SA协议规定转载,敬请在正文中标注并保留原文/译文链接和作者/译者等信息。

发布于 2024-12-28
195
目录

    推荐阅读