建议:迁移到Go 2

建议:迁移到Go 2

本文翻译自《Proposal: Go 2 transition》。

作者:Ian Lance Taylor

最后更新时间:2018年10月15日

摘要

本文是关于如何在尽可能少地破坏的情况下,从Go 1迁移到Go 2进行不兼容更改的建议。

背景

目前Go语言和标准库都遵守Go 1兼容性保证。该文档的目标是承诺Go的新版本不会破坏现有的程序。

Go 2进展的目标之一是对会破坏兼容性保证的语言和标准库进行更改。由于Go是在分布式开源环境中使用的,因此我们不能依赖卖旗日(flag day)(译者注:指一种既不向前兼容也不向后兼容的软件更改,其制作成本高且撤消成本高)。我们必须允许使用不同版本的Go编写的不同包可以互相操作。

每种语言都会经历版本转换。作为背景,这里有一些关于其他语言所做的事情的记录。你可以随意跳过本节的其余部分。

C

C语言版本由ISO标准化过程驱动。C语言开发非常注重向后兼容性。在第一个ISO标准C90之后,每个后续标准都保持严格的向后兼容性。在引入新关键字的地方,它们被引入到C90保留的命名空间中(一个下划线后跟一个大写的ASCII字母),并且可以通过特定头文件中的#define宏访问它们,这些头文件以前并不存在(例如_Complex<complex.h>中定义为复数,而_Bool<stdbool.h>中定义为bool)。C90中定义的基本语言语义都没有改变。

此外,大多数C编译器都提供选项来精确定义代码应该针对哪个版本的C标准进行编译(例如,-std=c90)。大多数标准库实现都支持在包含头文件之前使用#define定义的特性宏,用来准确指定应该提供哪个版本的库(例如,_ISOC99_SOURCE)。虽然这些特性以前存在过Bug,但它们相当可靠并且被广泛使用。

这些选项的一个关键特性是,能让使用不同的语言版本和库版本编写的代码,通常都可以链接在一起并按照预期工作。

第一个标准C90确实对以前的C语言实现进行了重大更改,可以非正式地认为以前的C语言就是K&R C。引入了新关键字,例如volatile(实际上这可能是C90中唯一的新关键字)。 整数表达式中整数提升的精确实现从无符号保留(unsigned-preserving)更改为值保留(value-preserving)。幸运的是,很容易检测到由于使用了新关键字而编译错误的代码,并且很容易调整该代码。整数提升的变化实际上让新手用户没有那么难理解,有经验的用户大多使用显式转换,来确保在具有不同整数大小的系统之间的可移植性,因此虽然没有自动检测该问题,但在实践中并没有多少代码被破坏。

也有一些恼人的变化。C90引入了三字母词,它改变了一些字符串常量的行为。编译器适应了诸如-no-trigraphs-Wtrigraphs之类的选项。

更严重的是,C90引入了未定义行为(undefined behavior)的概念,并声明调用未定义行为的程序可能会采取任何操作。在K&R C中,被C90描述为未定义行为的情况大多被视为C90中所谓的依赖具体实现的行为(implementation-defined behavior):程序将采取一些不可移植但可以预测行为的操作。编译器编写者吸收了未定义行为的概念,并开始编写假定该行为不会发生的编译优化。这造成了令不熟悉C标准的人感到惊讶的影响。我不会在这里详细介绍,但其中一个示例(来自我的博客)是有符号数的栈溢出

当然C仍然是内核开发的首选语言和计算行业的胶水语言。尽管它已被更加新的语言部分取代了,但这并不是新版本的C做出的任何选择之过。

我在这里看到的教训是:

  • 向后兼容性很重要。
  • 小部分破坏兼容性是可以的,只要人们可以通过编译器选项或编译器错误发现这些破坏。
  • 可以选择特定语言/库版本的编译器选项很有用,前提是使用不同选项编译的代码可以链接在一起。
  • 没有限制的未定义行为会让用户感到困惑。

C++

C++语言的版本现在也由ISO标准化过程驱动。与C一样,C++也非常关注向后兼容性。历史上,C++在添加新关键字方面更加自由(C++ 11中有10个新关键字)。这很正常,因为较新的关键字往往相对较长(constexprnullptrstatic_assert),使得使用新关键字作为标识符的代码很容易找到编译错误。

C++使用与C中相同的选项来指定语言和库的标准版本。在未定义的行为方面,C++遇到与C相同的问题。

C++中一个突破性变化的例子是在for循环的初始化语句中声明的变量范围的变化。在C++的预标准版本中,该变量的范围扩展到for循环所在的封闭块的末尾,就好像它是在for循环之前声明的一样。在第一个C++标准C++ 98的开发过程中,对其进行了修改,使其范围仅限于for循环本身。编译器通过引入诸如-ffor-scope之类的选项进行了调整,以便用户可以控制变量的预期范围(在一段时间内,当既不使用-ffor-scope也不使用-fno-for-scope进行编译时,GCC编译器使用了旧的范围,但警告任何依赖这一行为的代码)。

尽管向后兼容性相对较强,但用新版本的C++(如C++ 11)编写代码往往与用旧版本的C++编写代码有着非常不同的感觉。这是因为样式已更改为使用新的语言和库功能。原始指针不太常用,使用范围循环而不是迭代器,诸如右值引用和移动语义等新概念被广泛使用,等等。熟悉C++旧版本的人很难理解用新版本编写的代码。

C++当然是一种非常流行的语言,正在进行的语言修改过程并没有损害它的流行性。

除了C的教训,我还想补充一点:

  • 在保持向后兼容的同时,新版本可能会有非常不同的感觉。

Java

与我讨论的其他语言相比,我对Java的了解较少,因此这里可能存在更多错误,当然也存在更多偏见。

Java在字节码级别很大程度上向后兼容,这意味着Java N+1版本的库可以调用由Java版本N(以及N-1、N-2等)编写和编译的代码。Java源代码也大多是向后兼容的,尽管它们会不时添加新的关键字。

Java文档非常详细地介绍了从一个版本迁移到另一个版本时可能出现的兼容性问题。

Java标准库非常庞大,每个新版本都会添加新包。包也会不时被弃用。使用已弃用的包将在编译时引发一个警告(警告可能会被关闭),并且在几次发布后,已弃用的包将被删除(至少在理论上是这样)。

Java似乎没有太多的向后兼容性问题。问题集中在JVM上:较旧的JVM通常不会运行较新版本的库,因此你必须确保你的JVM至少与你要使用的最新库所需的一样新。

Java按理说具有某种前向兼容性问题,因为JVM字节码提供了比CPU更高级别的接口,这使得引入不能使用现有字节码直接表示的新特性变得更加困难。

这种前向兼容性问题是Java泛型使用类型擦除的部分原因。更改现有字节码的定义会破坏已经编译成字节码的现有程序。扩展字节码以支持泛型类型需要定义大量额外的字节码。

从某种程度上说,这种前向兼容性问题对于Go来说并不存在。由于Go编译为机器代码,并通过生成额外的机器代码来实现所有必需的运行时检查,因此不存在类似的前向兼容性问题。

但总的来说:

  • 请注意兼容性问题可能会限制编程语言未来的更改。

Python

Python 3.0(也称为Python 3000)于2006年开始开发,最初于2008年发布。2018年过渡仍未完成。有些人继续使用Python 2.7(2010年发布)。这不是Go 2想要效仿的路径。

这种缓慢过渡的主要原因似乎是缺乏向后兼容性。Python 3.0故意与早期版本的Python不兼容。值得注意的是,print从语句更改为函数,字符串更改为使用Unicode。Python通常与C代码结合使用,后者的变化意味着任何将字符串从Python传递到C的代码都需要调整C代码。

因为Python是一种解释型语言,并且因为没有向后兼容性,所以不可能在同一个程序中混合使用Python 2和Python 3代码。这意味着对于使用一系列库的典型的程序,每个库都必须先转换为Python 3,然后才能转换程序。由于程序处于各种转换状态,库必须同时支持Python 2和3。

Python支持from __future__ import FEATURE形式的语句。像这样的语句以某种方式改变了Python对文件其余部分的解释。例如,from __future__ import print_functionprint从语句(如在 Python 2中)更改为函数(如在Python 3中)。这是一种渐进地把代码更新到新的语言版本的方式,并使在不同的语言版本之间共享相同代码变得更加容易。

因此我们知道:

  • 向后兼容性是必不可少的。
  • 与其他语言的接口的兼容性很重要。
  • 升级到新语言版本也受到你使用的代码库所支持的版本的限制。

Perl

Perl 6的开发过程始于2000年。Perl 6规范的第一个稳定版本于2015年发布。这不是Go 2想要效仿的路径。

这条道路如此缓慢有很多原因。Perl 6有意不向后兼容:它旨在修复语言中的缺陷。Perl 6旨在通过规范来表示,而不是像以前版本的Perl那样,通过实现来表示。Perl 6从一组更改建议开始,随着时间的推移会不断发展,再继续发展得更多。

Perl支持use feature,类似于Python的from __future__ import。它更改了文件其余部分的解释方式,以使用新语言的指定功能。

  • 不要成为Perl 6。
  • 设定并遵守最后期限。
  • 不要一下子改变一切。

建议

语言更改

迂腐地说,我们必须使用一种方式来谈论特定的语言版本。Go语言的每个更改首先出现在Go的发行版中。我们将使用Go版本号来定义语言版本。这是唯一合理的选择,但它可能会造成混淆,因为标准库的更改也与Go版本号相关联。在考虑兼容性时,有必要在概念上将Go语言版本与标准库版本分开。

作为特定更改的一个示例,类型别名(type aliase)首先在Go语言1.9版本中可用。类型别名是向后兼容语言更改的一个示例。所有用Go语言1.0到1.8版本编写的代码在Go语言1.9版本继续与以前相同的方式工作。但使用类型别名的代码需要使用Go语言1.9或更高版本才能编译运行。

增加语言特性

类型别名是增加语言特性的一个示例。使用类型别名语法type A = B的代码无法在1.9版本之前的Go中编译。

类型别名和自Go 1.0以来的其他向后兼容的更改向我们表明,对于语言特性的增加,包没有必要显式声明它们所需的最低语言版本。一些包使用类型别名这一新特性。当用Go 1.8工具编译这样的包时,编译失败。包作者可以简单地说:升级到Go 1.9,或者降级到包的早期版本。Go工具不需要知道这个要求;因为无法使用旧版本的工具进行编译暗示了这一点。

程序员当然需要了解语言特性的增加,但工具不需要。Go 1.8工具和Go 1.9工具都不需要明确知道Go 1.9中增加了类型别名,除了Go 1.9编译器可以编译类型别名而Go 1.8编译器不能,这个有限的意义之外。指定最低语言版本以获得更好的不支持语言特性的错误消息的可能性,将在下文讨论。

移除语言特性

我们还必须考虑从语言中删除特性的语言更改。例如,issue 3939建议我们删除string(i)转换整数值i。如果我们在Go版本1.20中进行此更改,那么使用此语法的包将在Go 1.20中停止编译。(如果你更愿意将向后不兼容的更改限制在新的主版本中,那么在此讨论中将1.20替换为2.0;问题仍然存在。)

在这种情况下,使用旧语法的包没有简单的修改方法。虽然我们可以提供将1.20之前的代码转换为可工作的1.20代码的工具,但我们不能强制包作者运行这些工具。一些软件包可能没有维护但仍然有用。一些组织可能希望升级到1.20而不必重新验证他们所依赖的软件包的版本。一些软件包作者可能希望使用1.20,他们的软件包现在已经损坏,但没有时间修复他们的软件包。

这些场景表明我们需要一种机制,来指定可以用来构建软件包的Go语言的最高版本。

重要的是,指定Go语言的最高版本不应被视为要使用的Go工具的最高版本。随Go 1.20版本发布的Go编译器必须能够构建Go 1.19版本编写的包。这可以通过模仿C编译器支持的-std选项向编译器(以及,如果需要,汇编器和链接器)添加一个选项来完成。当编译器看到选项时,可能是-lang=go1.19,它将使用Go 1.19语法来编译代码。

这需要编译器以某种方式支持所有以前的版本。如果证明支持旧语法很麻烦,则可以通过将代码从旧版本转换到当前版本来实现-lang选项。这将使对旧版本的支持不在编译器的合理范围内,并且转换器对于想要更新其代码的人可能很有用。支持旧的语言版本不太可能会成为一个重大问题。

当然,即使包是用语言版本1.19的语法构建的,它在其他方面也必须是1.20版本的包:它必须与1.20版本的代码链接,能够调用和被1.20版本的代码调用,等等。

go工具需要知道最大语言版本,以便它知道如何调用编译器。我们继续考虑模块,此信息的逻辑位置位于go.mod文件。模块M的go.mod文件可以为其定义的包指定最大语言版本。当M作为其他模块的依赖项被下载时,它的最大语言版本将会被遵循。

最高语言版本不是最低语言版本。如果一个模块需要1.19版本的语言特性,但可以用1.20构建,我们可以说最大语言版本是1.20。如果我们使用Go 1.19版本来构建,我们低于最大版本值,但使用1.19语言版本来构建不是不可以。可以忽略大于当前工具支持的最大语言版本。如果我们稍后使用Go 1.21版本来构建该模块,我们可以使用-lang=go1.20选项使用最高1.20版本的语言特性。

这意味着这些工具可以自动设置最大语言版本。当我们使用Go 1.30发布模块时,我们可以将模块标记为具有最大语言版本1.30。该模块的所有用户都会看到这个最高版本并做正确的事情。

这意味着我们将不得不无限期地支持该语言的旧版本。如果我们在1.25版本之后删除了一个语言特性,如果使用-lang=go1.25选项(或-lang=go1.24或任何其他包含支持功能)。当然,如果没有使用-lang选项,或者选项是-lang=go1.26或更高版本,则该功能将不可用。由于我们不希望大规模删除现有语言功能,因此这应该是一个可管理的负担。

我相信这种方法足以实现移除语言特性

最小语言版本

为了获得更好的错误消息,允许模块文件指定最低语言版本可能很有用。但这不是必需的:如果一个模块使用了语言版本1.N中引入的特性,那么用1.N-1版本构建它将编译失败。这可能令人困惑,但在实践中,问题很可能是显而易见的。

也就是说,如果模块可以指定最低语言版本,那么在使用1.N-1构建时,go工具可以立即生成一条清晰的错误消息。

最低语言版本可能由编译器或其他工具设置。编译每个文件时,查看它使用的特性,并使用这些特性确定最低语言版本。不需要很精确。

这只是建议,不是要求。随着Go语言的变化,它可能会提供更好的用户体验。

语言重定义

Go语言也能够以不添加或删除的方式进行更改,即对特定语言结构的工作方式进行更改。例如,在Go 1.1中,64位主机上int类型的大小从32位更改为64位。这一变化相对无害,因为该语言本就没有指定int的确切大小。然而,一些Go 1.0程序使用Go 1.1进行编译后,可能会停止工作。

重新定义语言是这样一种情况,即我们的代码在版本1.N和版本1.M中都能成功编译,其中M>N,但两个版本中代码的含义不同。例如,issue 20733提出范围循环(range loop)中的变量应在每次迭代中重新定义。尽管在实践中,这种变化似乎更可能是一种修复程序而不是破坏程序,但从理论上来说,这种变化可能会破坏某些程序。

请注意,新关键字通常不会导致语言重新定义,但我们必须小心确保在引入新关键字之前确实如此。例如,如果我们按照错误处理草案设计中的建议引入关键字 check,并且我们允许像check(f())这样的代码,如果check被定义为同一个包中的函数名,这可能看起来是一个语言重新定义。但是在引入check关键字之后,任何定义这样一个函数名的尝试都将失败。因此,使用check的代码无论在何种意义上都不可能同时使用1.N和1.M版本进行编译。新关键字可以作为移除语言特性(check作为非关键字使用时)或添加语言特性(check是关键字时)来处理。

为了让Go生态系统在向Go 2的过渡中幸存下来,我们必须尽量减少此类语言重新定义。如前所述,成功的语言通常基本上没有超出特定程度的重新定义。

当然,语言重新定义的复杂性在于我们不再能依赖编译器来检测问题。查看重新定义的语言结构时,编译器无法知道其含义。在存在重新定义的语言结构时,我们也无法确定最大语言版本。因为我们不知道该语言结构是打算用旧含义还是新含义进行编译。

唯一的可行性可能是让程序员设置软件包的语言版本。在这种情况下,它可能是最低或最高语言版本,视情况而定。它必须以不会被任何工具自动更新的方式设置。当然,设置这样的版本很容易出错。随着时间的推移,最大的语言版本会导致令人惊讶的结果,因为人们试图使用新的语言功能,但都失败了。

我认为唯一可行的安全方法是不允许重新定义语言。

我们被当前的语义所困扰。这并不意味着我们无法改进它们。例如,对于issue 20733,即range问题,我们可以更改range循环,以便禁止获取range参数的地址,或从函数字面值中引用它。这不是一个语言重新定义;这将是一个语言特性移除。这种方法可能会消除Bug,而不会意外破坏代码。

构建标签(Build tags)

构建标签是一种现有机制,程序可以使用它来根据要发布的程序版本选择要编译的文件。

构建标签可用于给出发布的程序的版本号,它们看起来就像Go语言的版本号,但是,学究式地说,它们是不同的。在上面的讨论中,我们讨论了使用Go版本 1.N的编译器来编译Go语言版本为1.N-1的代码。使用构建标签是不可能做到的。

构建标签可设置用于编译特定文件的最大版本号或最小版本号,或同时设置两者。这是一种方便的方式来利用只有在特定版本之后才可用的语言更改;也就是说,这可用于在编译文件时设置最低语言版本号。

如上所述,对于语言更改最有用的是可以根据这种更改设置最大语言版本。构建标签并没有以有用的方式提供这一点。如果使用构建标签将当前版本设置为最大版本,则你的包将不会再生成以后的版本。只有将最高语言版本号设置为当前版本之前的版本号时,才能设置最高语言版本,并且还需要一个用于发布后续版本的当前包的副本。也就是说,如果你使用1.N进行构建,那么使用!1.N+1构建标签是没有帮助的。你可以使用!1.M构建标签,其中M<N,并且在几乎所有情况下,你都还需要一个单独的文件,其构建标记为!1.M+1

构建标签可用于处理语言重新定义:如果语言版本1.N有语言重新定义,程序员可以使用!1.N构建标签,使用旧语义和使用一个1.N构建标签的不同文件。然而,这些重复实现需要大量的工作,一般来说很难知道何时需要,而且很容易出错。构建标签的可用性不足以克服先前关于不允许任何语言重新定义的讨论。

导入“go2”

可以为Go添加一种机制,类似于Python的from __future_import和Perl的use feature。例如,我们可以使用一个特殊的导入路径,import "go2/type-aliases"。这将把所需的语言功能放在使用它们的文件中,而不是隐藏在go.mod文件中。

这将提供一种方法来描述文件所需的语言特性添加集合。它更复杂,因为它不依赖于语言版本,而是将语言分解为单独的功能。没有明显的办法消除这些特殊import,因此它们会随着时间的推移而积累。Python和Perl通过故意进行向后不兼容的更改来避免累积的问题。在转到Python3或Perl6之后,可以丢弃累积的特性。由于Go试图避免向后不兼容,因此没有明确的方法可以消除这些导入。

此机制无法处理移除语言特性的情况。我们可以引入删除导入,例如import "go2/no-int-to-string",但不清楚为什么会有人使用它。实际上,根本没有办法删除语言特性,即使是那些容易混淆和出错的特性。

这种方法似乎不适合Go。

标准库改变

迁移到Go 2的好处之一是有机会发布一些兼容Go 1的标准库包。另一个好处是有机会将许多(也许是大部分)软件包移出六个月的发布周期。如果模块实验成功,甚至有可能尽早开始做这件事,使一些包的发布周期更快。

我建议继续保持六个月的发布周期,但将其视为编译器/运行时的发布周期。我们希望Go发行版开箱即用,因此发行版将继续包含与今天大致相同的软件包集合的当前版本。然而,其中许多包实际上有它们自己的发布周期。使用给定Go版本的人将能够明确选择使用标准库包的新版本。事实上,在某些情况下,他们可能会使用旧版本的标准库包。

不同的发布周期需要包维护者投入更多的资源。只有当我们有足够的人手来管理它,有足够的测试资源来测试它时,我们才能做到这一点。

我们还可以继续对所有内容使用六个月的发布周期,但将可分离的包单独提供兼容的、不同的版本。

核心标准库

尽管如此,标准库的某些部分仍必须被视为核心库。这些库与编译器和其他工具紧密相关,必须严格遵循发布周期。不得使用这些库的旧版本或新版本。

理想情况下,这些库将保留在当前版本1上。如果似乎有必要将其中任何一个更改为版本2,则必须根据具体情况进行讨论。目前我看不出有什么理由这样做。

核心库的暂定清单是:

  • os/signal
  • plugin
  • reflect
  • runtime
  • runtime/cgo
  • runtime/debug
  • runtime/msan
  • runtime/pprof
  • runtime/race
  • runtime/tsan
  • sync
  • sync/atomic
  • testing
  • time
  • unsafe

我可能乐观地从这个列表中省略了net、os和syscall包。我们将看看我们能管理什么。

伴生标准库

伴生标准库是那些包含在Go发行版中但独立维护的包。当前标准库中的大部分内容其实都是这种包。这些软件包将遵循与今天相同的规则,并可选择在适当的情况下迁移到v2。可以使用go get升级或降级这些标准库包。特别是,这些包可以独立于每六个月的核心库发布周期,修复Bug发布自己的次版本。

go工具必须能够区分核心库和伴生库。我不知道这将如何运作,但它似乎是可行的。

将标准库包移动到v2时,必须规划好同时使用包的v1和v2版本的程序。这些程序必须按预期运行,如果不可能,就必须干净利落地迅速运行失败。在某些情况下,这将涉及修改v1版本以使用也由v2版本使用的核心库。

标准库包必须能够使用Go语言的旧版本进行编译,至少是我们目前支持的前两个发布周期的Go语言版本。

从标准库中移除包

标准库包支持的go get能力将允许我们从发布中删除包。这些包将继续存在和维护,人们能够在需要时检索它们。但是,默认情况下它们不会随Go版本一起发布。

这将包括像以下这样的包:

  • index/suffixarray
  • log/syslog
  • net/http/cgi
  • net/http/fcgi

以及其他似乎没有被广泛使用的包。

我们应该在适当的时候为旧包制定弃用政策,将这些包设置为不再维护。弃用政策也适用于移至v2版本的v1版本的软件包。

或者这可能被证明是有问题的,我们不应该弃用任何现有的包,也不应该将它们从标准的Go发行版本中删除。

Go 2

如果上述过程按计划进行,那么在某种重要意义上永远不会有Go 2。或者,换句话说,我们将慢慢过渡到新的语言和库特性。我们可以在过渡期间的任何时候决定我们现在是Go 2,这可能是一个很好的营销方式。或者我们可以跳过它(从来没有C 2.0,为什么要有Go 2.0?)。

C、C++和Java等流行语言从来没有v2版本。实际上,它们始终处于版本1.N,尽管它们使用不同的名称来称呼该状态。我认为我们应该效仿它们。事实上,从不兼容的新版本语言或核心库的意义上说,完全意义上的Go 2对我们的用户来说不是一个很好的选择。不夸张地说,一个真正的Go 2版本可能是有害的。

开发和发布模块

本文翻译自《Developing and publishing modules》。

目录

开发和发布模块的工作流程

设计和开发

分布式发布

发现包

版本控制

你可以将相关的包收集到模块中,然后发布模块供其他开发人员使用。本主题概述开发和发布模块。

要支持开发、发布和使用模块,请使用:

  • 开发和发布模块,并随时间使用新版本对其进行修改的工作流。参见开发和发布模块的工作流
  • 帮助模块用户理解并以稳定的方式升级到新版本的设计实践。参见设计和开发
  • 用于发布模块和检索其代码的分布式去中心化系统。你可以让其他开发人员从自己的存储库中使用你的模块,并使用版本号发布。请参见分布式发布
  • 一个软件包搜索引擎和文档浏览器(pkg.go.dev),开发人员可以在其中找到你的模块。请参阅发现包
  • 模块版本编号规约,用于向使用你的模块的开发人员传达稳定性和向后兼容性的信息。请参见版本控制
  • 让其他开发人员更容易管理依赖关系的Go工具,包括获取模块的源代码、升级等。请参阅管理依赖项

另请参阅

开发和发布模块的工作流程

当你想为其他人发布你的模块时,你可以采用一些约定来让这些模块更容易使用。

模块发布和版本控制工作流程中更详细地描述了以下高级步骤。

1 设计并编写模块将包含的软件包。

2 根据规约将代码提交到你的存储库,以确保其他人可以通过Go工具使用该代码。

3 发布模块以使开发人员能够发现它。

4 随着时间的推移,使用版本编号规约来修订模块,可以从版本编号中看出每个版本的稳定性和向后兼容性。

设计和开发

如果你的模块中的函数和包形成一个连贯的整体,那么开发人员将更容易找到和使用你的模块。当你设计模块的公共API时,请尽量使其功能目标明确且各不相关(译者注:高内聚,低耦合)。

此外,在设计和开发模块时考虑向后兼容性,有助于用户升级,同时最大限度地减少对用户代码的干扰。你可以在代码中使用某些技术来避免发布破坏向后兼容性的版本。有关这些技术的更多信息,请参阅Go博客上的保持模块兼容

在发布模块之前,可以使用replace指令在本地文件系统上引用它。这使得在模块仍在开发中时,编写调用其内函数的客户端代码更加容易。有关详细信息,请参阅模块发布和版本控制工作流程中的“针对未发布的模块进行编程”。

分布式发布

在Go中,你通过在存储库中为模块的代码打标签来发布模块,以便其他开发人员使用。你不需要将模块推送到一个集中式服务器,因为Go工具可以直接从存储库(使用模块路径,该路径是省略了网络协议的URL)或代理服务器下载模块。

在代码中导入包后,开发人员使用Go工具(包括go get命令)下载模块的源代码进行编译。为了支持此模式,你应该遵循惯例和最佳实践,使Go工具(代表另一个开发人员)可以从存储库中检索模块的源代码。例如,Go工具使用你模块的模块路径,以及用于标记模块以供发布的模块版本号,为其用户定位和下载你的模块。

有关源代码和发布约定以及最佳实践的更多信息,请参阅管理模块的源代码

有关发布模块的分步说明,请参阅发布一个模块

发现包

在你发布了模块并有人使用Go工具获取了它之后,它将在pkg.go.dev这个Go包发现站点上可见。在那里,开发人员可以搜索该站点找到它并阅读它的文档。

为了开始使用模块,开发人员从模块中导入包,然后运行go get命令下载其源代码进行编译。

有关开发人员如何查找和使用模块的更多信息,请参阅管理依赖项

版本控制

随着时间的推移,当你修改和改进模块时,你会分配版本号(基于语义版本控制模型),以表示每个版本的稳定性和向后兼容性。这有助于使用你的模块的开发人员确定模块何时稳定,以及升级模块是否在代码行为上有重大变化。你可以通过在存储库中打标签模块的源代码,来指派模块的版本号。

有关开发模块主版本更新的详细信息,请参阅开发一个主版本更新

有关如何为Go模块使用语义版本号模型的更多信息,请参阅模块版本编号

go.mod文件简明参考手册

本文翻译自《go.mod file reference》。

目录

例子

模块

语法

例子

笔记

go

语法

例子

笔记

需求(require)

语法

例子

笔记

替换(replace)

语法

例子

笔记

排除(exclude)

语法

例子

笔记

回收(retract)

语法

例子

笔记

每个Go模块都由一个go.mod文件定义,该文件描述模块的属性,包括它对其他模块和Go版本的依赖关系。

这些属性包括:

  • 当前模块的模块路径。这应该是Go工具可以从中下载模块代码的位置,例如模块代码的存储库的位置。当与模块的版本号结合使用时,它用作唯一标识符。它也是模块中所有包的包路径的前缀。有关Go如何定位模块的更多信息,请参阅Go模块参考手册
  • 当前模块所需的最低Go版本
  • 当前模块所需的其他模块的最低版本的一个列表。
  • 可选地,用另一个模块版本或本地目录替换所需模块,或排除所需模块的特定版本。

当你运行go mod init命令时,Go会生成一个go.mod文件。以下示例创建一个go.mod文件,将模块的模块路径设置为example/mymodule

$ go mod init example/mymodule

使用go命令来管理依赖项。这些命令确保go.mod文件中描述的需求保持一致,并且go.mod文件的内容有效。这些命令包括go getgo mod tidy以及go mod edit命令。

有关go命令的参考,请参阅Command go。你可以通过键入go help command-name从命令行获得帮助,就像go help mod tidy一样。

另请参阅

  • 当你使用Go工具来管理依赖项时,Go工具会对你的go.mod文件进行更改。有关更多信息,请参阅管理依赖项
  • 有关go.mod文件的更多详细信息和约束,请参阅Go模块参考手册

例子

go.mod文件包含指令,如下所示。这些将在本主题的其他地方进行描述。

module example.com/mymodule

go 1.14

require (
    example.com/othermodule v1.2.3
    example.com/thismodule v1.2.3
    example.com/thatmodule v1.2.3
)

replace example.com/thatmodule => ../thatmodule
exclude example.com/thismodule v1.3.0

模块

声明模块的模块路径,这是模块的唯一标识符(与模块版本号组合使用时)。模块路径成为模块包含的所有包的导入前缀。

有关更多信息,请参阅Go模块参考手册中的module指令

语法

module module-path

其中module-path代表模块的模块路径,通常是Go工具可以从中下载模块代码的存储库的位置。对于模块v2及更高版本,模块路径必须以主版本号结尾,例如/v2

例子

以下示例中的example.com代表可以从中下载模块代码的存储库的域名。

v0或v1模块的模块声明:

module example.com/mymodule

v2模块的模块路径:

module example.com/mymodule/v2

笔记

模块路径必须能唯一标识你的模块。对于大多数模块,其路径是一个URL,go命令可以在其中找到模块的代码(或重定向到模块的代码)。对于永远不会直接被下载的模块,模块路径可以是由你决定的某个名称,确保唯一性即可。前缀example/保留用于示例。

有关详细信息,请参阅管理依赖项

实际上,模块路径通常是模块源代码的存储库的域名和模块代码在存储库中的路径。go命令在下载模块版本或解决依赖关系时,就依赖于此形式。

即使你一开始并不打算让你的模块可供其他人使用,按照此形式确定其存储库路径也是一种最佳实践,这将帮助你在以后发布模块时避免重命名该模块。

如果一开始你不知道模块的最终存储库位置,请暂时使用安全的替代品,例如你拥有的域名或你能控制的名称(例如你的公司名称),然后紧跟着模块的名称或源代码的目录名。有关更多信息,请参阅管理依赖项

例如,如果你在stringtools目录中进行开发,你的临时模块路径可以是<company-name>/stringtools,如下例所示,其中company-name是你公司的名称:

go mod init <company-name>/stringtools

go

表示模块是在go指令指定的Go版本下编写的。

有关更多信息,请参阅Go模块参考手册中的go指令

语法

go minimum-go-version

其中minimum-go-version代表编译此模块中的包所需的最低的Go版本。

例子

模块必须在Go 1.14或更高版本上运行:

go 1.14

笔记

go指令最初旨在支持对 Go 语言的向后不兼容更改(请参阅Go 2转换)。自从引入模块以来没有不兼容的语言更改,但go指令仍然影响新语言功能的使用:

  • 对于模块中的包,编译器拒绝使用在go指令指定的版本之后引入的语言功能。例如,如果一个模块有指令go 1.12,它的包可能不会使用像1_000_000这样的数值字面量,因为这是在Go 1.13中引入的语言新特性。
  • 如果用较旧的Go版本构建模块的其中一个包,并遇到编译错误,则该错误会指出该模块是为较新的Go版本编写的。例如,假设一个模块有go 1.13指令,并且一个包使用了数值字面量1_000_000。如果该包使用Go 1.12来构建,编译器会注意到代码是为Go 1.13编写的。

此外,go命令根据go指令指定的版本号更改其行为。这具有以下效果:

  • go 1.14或更高版本中,可以启用自动vendor(译者注:vendor模式的模块管理属于Go语言在1.11版本之前的其中一种模块管理方式,Go 1.11引入的基于go.mod文件的模块管理方式才是最先进的模块管理方式)。如果文件vendor/modules.txt存在并且与go.mod一致,则无需显式使用-mod=vendor标志。
  • go 1.16或更高版本中,all包模式仅匹配主模块(main模块)中的包和测试导入的包。这是自引入模块以来由go mod vendor保留的同一组包。在较低Go版本中,all还包括主模块中导入的包及其测试文件等等。
  • go 1.17或更高版本中:
    • go.mod文件为每个模块包含一个明确的require指令,该指令提供由主模块中的包或测试文件导入的任何包。(在go 1.16及更低版本中,仅当最小版本选择会选择不同版本时才会包含间接依赖。)此额外信息支持模块图化简模块懒加载
    • 因为可能比以前的go版本有更多// indirect,间接依赖被记录在go.mod文件中的一个单独的块中。
    • go mod vendor会忽略vendor目录中依赖项的go.mod和go.sum文件。(这允许在vendor的子目录中调用go命令以识别正确的主模块。)
    • go mod vendor将每个依赖项的go.mod文件中的Go版本记录在vendor/modules.txt文件中。

go.mod文件最多可以包含一个go指令。大多数命令都会在当前Go版本中添加一个go指令(如果还不存在的话)。

需求(require)

将模块声明为当前模块的依赖项,指定所需模块的最低版本。

有关更多信息,请参阅Go模块参考手册中的require指令

语法

require module-path module-version

module-path代表模块的模块路径,通常是模块源代码的存储库的域名再加上模块名称。对于模块版本v2及更高版本,此值必须以主版本号结尾,例如/v2

module-version代表模块的版本号。这可以是发行版的版本号,例如v1.23,也可以是Go生成的伪版本号,例如v0.0.0-20200921210052-fa0125251cc4。

例子

需求已发布的版本v1.2.3:

require example.com/othermodule v1.2.3

需求在其存储库中尚未加标签的版本,使用Go工具生成伪版本号:

require example.com/othermodule v0.0.0-20200921210052-fa0125251cc4

笔记

当你运行go命令(例如go get)时,go命令会插入包含导入包的每个模块的指令。当模块在其存储库中尚未加标签时,Go会分配一个伪版本号,它会在你运行命令时自动生成。

通过使用replace指令,你可以让Go从存储库以外的位置需求模块。

有关版本号的更多信息,请参阅模块版本编号

有关管理依赖关系的详细信息,请参阅以下内容:

添加一个依赖项

获取特定依赖项的某个版本

发现可用更新

升级或降级一个依赖项

同步你的代码的依赖项

替换(replace)

用另一个模块版本或本地目录替换特定模块版本(或所有版本)的内容。Go工具将在解析依赖关系时使用替换路径。

有关更多信息,请参阅Go模块参考手册中的replace指令

语法

replace module-path [module-version] => replacement-path [replacement-version]

module-path代表要替换的模块路径。

module-version,可选,代表要替换的特定版本号。如果省略此版本号,则模块的所有版本都将被替换为箭头右侧的内容。

replacement-path代表Go应查找的所需模块的路径。这可以是模块路径,也可以是本地文件系统上的模块源代码所在目录的路径。如果这是模块路径,则必须指定replacement-version。如果这是本地路径,则不能使用replacement-version

replacement-version,可选,代表用来替换的模块的版本号。只有当replacement-path 是模块路径(而不是本地目录路径)时,才能指定replacement-version

例子

  • 被替换为模块存储库的分叉(fork)版本

在以下示例中,example.com/othermodule的任何版本都将被替换为其分支版本。

require example.com/othermodule v1.2.3
replace example.com/othermodule => example.com/myfork/othermodule v1.2.3-fixed

用另一个模块路径替换一个模块时,不要更改被替换模块里的包的导入语句。

有关使用模块代码的分叉版本的更多信息,请参阅从自己的存储库分叉版本请求外部模块代码

  • 被替换为模块的其他版本

以下示例指定使用v1.2.3版本,而不是该模块的任何其他版本。

require example.com/othermodule v1.2.2
replace example.com/othermodule => example.com/othermodule v1.2.3

以下示例将模块v1.2.5版本替换为同一模块的v1.2.3版本。

replace example.com/othermodule v1.2.5 => example.com/othermodule v1.2.3
  • 被替换为本地代码

以下示例指定使用本地目录里的代码替换模块的所有版本。

require example.com/othermodule v1.2.3
replace example.com/othermodule => ../othermodule

以下示例指定本地目录里的代码仅替换v1.2.5版本。

require example.com/othermodule v1.2.5
replace example.com/othermodule v1.2.5 => ../othermodule

有关使用模块代码的本地副本的更多信息,请参阅需求本地目录中的模块代码

笔记

如果要使用其他路径来查找模块的源,请使用replace指令将模块路径值临时替换为其他值。这具有将Go对模块的搜索重定向到替换的位置的效果。不需要更改包导入路径即可使用替换路径。

可以使用excludereplace指令来控制构建时依赖关系的解析。这些指令在依赖当前模块的模块中会被忽略。

replace指令在以下情况下很有用:

  • 你正在开发一个新模块,其代码尚未在存储库中。你希望使用客户端对本地版本进行测试。
  • 你发现了某个依赖项的一个问题,你克隆该依赖项的存储库到本地,修复这个问题,并使用该本地存储库测试修复是否成功。

请注意,单独的replace指令不会将模块添加到模块图中。require指令还需要引用在主(main)模块的go.mod文件或依赖项的go.mod文件中被替换的模块版本。如果没有要替换的特定版本,可以使用假版本,如下例所示。请注意,这将破坏依赖于该模块的模块,因为replace指令仅应用于主模块。

require example.com/mod v0.0.0-replace
replace example.com/mod v0.0.0-replace => ./mod

有关替换所需的模块(包括使用Go工具进行更改)的更多信息,请参阅:

有关版本号的更多信息,请参阅模块版本编号

排除(exclude)

指定要从当前模块的依赖关系图中排除的模块或模块版本。

有关详细信息,请参阅Go模块参考手册中的exclude指令

语法

exclude module-path module-version

module-path代表要排除的模块的模块路径。

module-version代表特定的版本号。

例子

排除example.com/their模块版本v1.3.0:

exclude example.com/theirmodule v1.3.0

笔记

使用exclude指令排除间接需求但由于某种原因无法加载的模块的特定版本。例如,你可以使用它排除校验和(checksum)无效的模块版本。

在构建当前模块(主模块)时,使用excludereplace指令来控制对构建时依赖关系的解析。这些指令在依赖于当前模块的模块中被忽略。

你可以使用go mod edit命令来排除模块,如下例所示。

go mod edit -exclude=example.com/[email protected]

有关版本号的更多信息,请参阅模块版本编号

撤回(retract)

指示由go.mod定义的模块的版本或某个范围的版本不应该被依赖。当版本未成熟就发布或发布版本后发现严重问题时,retract指令非常有用。

有关更多信息,请参阅Go模块参考手册中的retract指令

retract version // rationale
retract [version-low,version-high] // rationale

version代表要撤回的单个版本。

version-low代表要撤回的版本范围的下限。

version-high代表要撤回的版本范围的上限。version-lowversion-high都包含在范围内。

rationale是解释撤回原因的可选注释。可能显示在发给用户的消息中。

例子

撤回单个版本:

retract v1.1.0 // 意外发布。

撤回一个范围的版本:

retract [v1.0.0,v1.0.5] // 在某些平台会破坏构建。

笔记

使用retract指令指示某个模块的早期版本不应被使用。用户无法使用go getgo mod tidy或其他命令自动升级到被撤回的版本。用户使用go list -m -u命令将在可更新到的版本列表中看不到被撤回的版本。

撤回的版本应该保持可用,以便已经依赖它们的用户能够构建他们的包。即使从源存储库中删除了撤回的版本,它也可能在proxy.golang.org等镜像上仍然可用。依赖撤回的版本的用户在相关模块上运行go getgo list -m -u时可能会收到通知。

go命令通过读取模块最新版本的go.mod文件中的retract指令来发现被撤回的版本。最新版本按优先顺序为:

1 其最高版本号(如果有的话)

2 其最高预发布版本号(如果有的话)

3 存储库默认分支顶端的伪版本号。

新增一个撤回版本时,几乎总是需要加标签一个新的更高的版本,以便go命令在模块的最新版本中看到retract指令。

你可以发布一个唯一目的是发出撤回信息的新版本。在这种情况下,该新版本也可以自行撤回。

例如,如果意外加标签v1.0.0,可以使用以下指令加标签v1.0.1:

retract v1.0.0 // 意外发布。
retract v1.0.1 // 只包含撤回指令。

不幸的是,一旦发布了版本,就无法更改。如果稍后为其他commit加标签v1.0.0,go命令可能会在go.sum或校验和数据库中检测到不匹配的值。

模块的撤回版本通常不会出现在go list -m -versions的输出中,但你可以使用-reacted选项来显示它们。有关更多信息,请参阅Go模块参考手册中的go list -m

模块版本编号

本文翻译自《Module version numbering》。

目录

开发过程中

伪版本号

v0版本号

预发布版本号

次版本号

补丁程序版本号

主版本号

模块的开发人员使用模块版本号的每个部分来表示版本的稳定性和向后兼容性。对于每个新版本,模块的版本号具体反映了自上一个版本以来模块更改的性质。

当你开发使用外部模块的代码时,可以在升级时使用版本号来了解外部模块的稳定性。当你开发自己的模块时,你的版本号将向其他开发人员指示模块的稳定性和向后兼容性。

本主题描述模块版本号的含义。

另请参阅

  • 当你在代码中使用外部包时,可以使用Go工具管理这些依赖关系。有关详细信息,请参见管理依赖关系
  • 如果你正在开发供其他人使用的模块,则在发布模块时使用版本号,并在其代码存储库中加标签。有关详细信息,请参见发布一个模块

发布的模块使用语义版本号控制模型给出的版本号,如下图所示:

下表描述了版本号的各个部分如何表示模块的稳定性和向后兼容性。

各阶段的版本号示例给开发者的信息
开发过程中自动给出伪版本号 v0.x.x表明模块仍在开发中且不稳定。此版本没有向后兼容性或稳定性保证。
主版本号(Major version)v1.x.x表示向后不兼容的公共API更改。此版本不保证它将与以前的主要版本向后兼容。
次版本号(Minor version)vx.4.x表示向后兼容的公共API更改。此版本保证了向后兼容性和稳定性。
补丁程序版本号(Patch version)vx.x.1表示不影响模块的公共API或其依赖关系的更改。此版本保证了向后兼容性和稳定性。
预发布版本号(Pre-release version)vx.x.x-beta.2表明这是一个预发布里程碑,例如alphabeta。此版本没有稳定性保证。

开发过程中

表示该模块仍在开发中且不稳定。此版本不提供向后兼容性或稳定性保证。

版本号可以采用以下形式之一:

伪版本号(pseudo-version number)

v0.0.0-20170915032832-14c0d48ead0c

v0版本号

v0.x.x

伪版本号

当模块未在其代码存储库中打标签时,Go工具将生成一个伪版本号,供调用该模块中的函数的代码的go.mod文件使用。

注意:作为最佳实践,始终允许Go工具生成伪版本号而不是创建自己的版本号。

当使用该模块功能的代码开发人员需要针对尚未加语义版本号标签的commit进行开发时,伪版本号就很有用。

伪版本号由破折号分隔的三部分组成,如下表所示:

语法

baseVersionPrefix-timestamp-revisionIdentifier

部分

  • baseVersionPrefix(vX.0.0或vX.Y.Z-0)是从之前的语义版本号标签或从vX.0.0(如果没有语义版本号标签)派生的值。
  • timestamp(yymmddhhmms)是创建修订号的UTC时间。在Git中,这是提交时间,而不是作者的时间。
  • revisionIdentifier(abcdefabdedef)是commit哈希值的12个字符前缀,或者在Subversion中是用0填充的修订号。

v0版本号

使用v0编号发布的模块将具有正式的语义版本号,其中包含主、次和补丁部分,以及可选的预发布标识符。

虽然v0版本可以在生产中使用,但它不能保证稳定性或向后兼容性。此外,版本v1和更高版本允许破坏v0版本的代码的向后兼容性。因此,使用v0模块中的代码的开发人员应该明白将会有不兼容的更改,直到v1发布。

预发布版本号

表明这是一个预发布里程碑,如alpha或beta。此版本没有稳定性保证。

示例

vx.x.x-beta.2

模块的开发人员可以通过添加连字符和预发布标识符,与任何major.minor.patch组合成预发布版本号。

次版本号

表明模块的公共API具有向后兼容的更改。此版本保证向后兼容性和稳定性。

示例

vx.4.x

此版本更改了模块的公共API,但并未以破坏调用代码的方式进行。这可能包括更改模块自身的依赖项或添加新函数、方法、结构体字段或类型。

换句话说,此版本可能包含新功能以进行增强,其他开发人员可能想要使用这些新功能。但是,使用以前的次版本的开发人员无需更改他们的代码。

补丁程序版本号

表示不影响模块的公共API或其依赖项的更改。此版本保证向后兼容性和稳定性。

示例

vx.x.1

增加此数字的版本更新仅适用于很小的更改,如Bug修复。使用该版本的开发人员可以安全地升级到此版本,而无需更改代码。

主版本号

表明模块的公共API有向后不兼容的更改。此版本不保证它与以前的主版本向后兼容。

示例

v1.x.x

v1或更高版本号表示模块可稳定使用(它的预发布版本除外)。

请注意,因为版本0没有稳定性或向后兼容性保证,所以将模块从v0升级到v1的开发人员负责调整破坏向后兼容性的更改。

只有在必要时,模块开发人员才应该将这个数字增加到v1以上,因为版本升级对代码使用该模块中的功能的开发人员来说意味着严重的中断。这种中断包括对公共API向后不兼容的更改,以及从该模块导入包时可能需要更新包路径。

如果主版本更新到高于v1的版本,也会有一个新的模块路径。这是因为模块路径将附加主版本号,如下例所示:

module example.com/mymodule/v2 v2.0.0

主版本更新使其成为一个新模块,其历史与模块的先前版本不同。如果你正在开发要为其他人发布的模块,请参阅模块发布和版本控制工作流中的“发布破坏API的更改”。

有关模块指令的更多信息,请参阅go.mod参考

教程:Go入门

本文翻译自《Tutorial: Get started with Go》。

目录

先决条件

安装Go

写一些代码

在外部包中调用代码

写更多的代码

在本教程中,你将简要介绍Go编程。在此过程中,你将:

  • 安装Go(如果尚未安装)。
  • 编写一些简单的“Hello, world”代码。
  • 使用go命令运行你的代码。
  • 使用Go包工具查找可在你的代码中使用的包。
  • 调用外部模块的函数。

注意:有关其他教程,请参阅教程

先决条件

  • 一些编程经验。此处的代码非常简单,但有助于了解一些有关函数的知识。
  • 一个编辑代码的工具。你拥有的任何文本编辑器都可以正常工作。大多数文本编辑器都对Go有很好的支持。最受欢迎的是VSCode(免费)、GoLand(付费)和Vim(免费)。
  • 一个命令行终端。Go在Linux和Mac上的任何终端,以及Windows中的PowerShell或cmd上都能很好地工作。

安装Go

只需使用下载和安装步骤。

写一些代码

从Hello,World开始。

1 打开命令提示符并cd到家目录。

在Linux或Mac上:

cd

在Windows上:

cd %HOMEPATH%

2 为第一个Go源代码创建hello目录。

例如,使用以下命令:

mkdir hello
cd hello

3 为代码启用依赖项跟踪。

当你的代码导入包含在其他模块中的包时,你可以在自己的模块中管理这些依赖关系。该模块由go.mod文件定义,该文件跟踪提供这些包的模块。go.mod文件将保留在源代码中,包括源代码存储库中。

要通过创建go.mod文件来启用代码的依赖项跟踪,请运行go mod init命令,为其指定代码所在模块的名称。该名称会成为模块的模块路径。

在实际开发中,模块路径通常是保存源代码的存储库的位置。例如,模块路径可能是github.com/mymodule。如果你计划发布模块供其他人使用,则模块路径必须是Go工具可以下载到该模块的位置。有关使用模块路径命名模块的详细信息,请参阅管理依赖项

在本教程中,只需使用example/hello

$ go mod init example/hello
go: creating new go.mod: module example/hello

4 在文本编辑器中,创建一个文件hello.go,在其中编写代码。

5 将以下代码粘贴到hello.go文件中并保存该文件。

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

这是你的Go代码。在此代码中,你:

  • 声明一个main包(包是对函数进行分组的一种方式,它由同一目录中的所有文件组成)。
  • 导入流行的fmt,其中包含格式化文本的功能,包括打印到控制台。此软件包是安装Go时获得的标准库软件包之一。
  • 实现将消息打印到控制台的main函数。运行main包时,默认情况下会执行main函数。

6 运行你的代码以查看这句问候语。

$ go run .
Hello, World!

go run命令是你将使用Go完成任务的众多命令之一。使用以下命令获取其他列表:

$ go help

在外部包中调用代码

当你需要你的代码执行其他人可能已实现的操作时,你可以寻找具有对应功能的第三方包。

1 使用外部模块的功能使你打印的消息更有趣。

1) 访问pkg.go.dev并搜索“quote”包

2) 在搜索结果中找到并单击rsc.io/quote包(如果你看到rsc.io/quote/v3,请暂时忽略它)。

3) 在“文档Documentation”部分的“索引Index”下,记下你可以从代码中调用的函数列表。你将使用Go函数。

4) 在本页顶部,请注意包quote含在rsc.io/quote模块中。

你可以使用pkg.go.dev站点查找已发布的模块,这些模块的包中包含你可以在自己的代码中使用的功能。包在模块中发布,比如rsc.io/quote。随着时间的推移,新版本对模块进行了改进,你可以升级代码以使用改进的版本。

2 在Go代码中,导入rsc.io/quote包并调用其Go函数。

添加高亮显示的行后,代码应包括以下内容:

package main

import "fmt"

import "rsc.io/quote"

func main() {
    fmt.Println(quote.Go())
}

3 添加新的模块需求和go.sum。

Go将根据需要添加rsc.io/quote模块,以及用于验证模块的go.sum文件。有关更多信息,请参阅Go modules Reference中的Authenticating modules

$ go mod tidy
go: finding module for package rsc.io/quote
go: found rsc.io/quote in rsc.io/quote v1.5.2

4 运行代码以查看所调用函数生成的消息。

$ go run .
Don't communicate by sharing memory, share memory by communicating.

请注意,你的代码调用Go函数,打印关于通信的巧妙消息。

当你运行go mod tidy时,它找到并下载了包含你导入的包的rsc.io/quote模块。默认情况下,它下载最新版本——v1.5.2。

写更多的代码

通过这篇快速入门教程,你安装了Go并学习了一些基本知识。要使用其他教程编写更多代码,请查看创建一个Go模块

总结

本文翻译自《Conclusion》。

在本教程中,你编写了封装到两个模块中的函数:一个发送问候语;另一个作为第一个的消费者。

注意:本主题是从《创建一个Go模块》开始的多部分教程的一部分。

有关管理代码中的依赖项的更多信息,请参阅管理依赖项。有关开发供他人使用的模块的更多信息,请参阅开发和发布模块

有关Go语言的更多功能,请查看Go之旅

编译和安装这个应用程序

本文翻译自《Compile and install the application》。

在最后一个主题中,你将学习几个新的go命令。go run命令是在你频繁更改代码时,快捷地编译和运行程序的方式,但它不会生成二进制可执行文件。

本主题介绍了两个用于构建代码的附加命令:

注意:本主题是从《创建一个Go模块》开始的多部分教程的一部分。

1 从hello目录中的命令行运行go build命令,将代码编译为可执行文件。

$ go build

2 从hello目录中的命令行运行新的hello可执行文件,以确认代码有效。

请注意,你的结果可能会有所不同,具体取决于你是否在测试后更改了greetings.go里的代码。

在Linux或Mac上:

$ ./hello
map[Darrin:Great to see you, Darrin! Gladys:Hail, Gladys! Well met! Samantha:Hail, Samantha! Well met!]

在Windows上:

$ hello.exe
map[Darrin:Great to see you, Darrin! Gladys:Hail, Gladys! Well met! Samantha:Hail, Samantha! Well met!]

你已将应用程序编译为可执行文件,因此你可以运行它。但是当前要运行它,你的命令提示符需要位于可执行文件的目录中,或者指定可执行文件的路径。

接下来,你将安装可执行文件,这样你就可以在不指定路径的情况下运行它。

3 发现Go安装路径,go命令将在其中安装当前包。

你可以通过运行go list命令来发现安装路径,如以下示例所示:

$ go list -f '{{.Target}}'

例如,命令的输出可能是/home/gopher/bin/hello,这意味着二进制文件将安装到/home/gopher/bin。你在下一步中将需要此安装目录。

4 将Go安装目录添加到系统的shell路径中。

这样,你就可以运行程序的可执行文件而无需指出可执行文件的位置。

在Linux或Mac上,运行以下命令:

$ export PATH=$PATH:/path/to/your/install/directory

在Windows上,运行以下命令:

$ set PATH=%PATH%;C:\path\to\your\install\directory

作为替代方案,如果你的shell路径中已经有一个类似$HOME/bin的目录,并且你想在那里安装你的Go程序,你可以通过使用go env命令设置GOBIN环境变量来更改安装目标:

$ go env -w GOBIN=/path/to/your/bin

$ go env -w GOBIN=C:\path\to\your\bin

5 更新shell路径后,运行go install命令编译并安装包。

$ go install

6 只需键入名称即可运行你的应用程序。为了让它变得有趣,打开一个新的命令提示符窗口并在其他目录中运行hello可执行文件。

$ hello
map[Darrin:Hail, Darrin! Well met! Gladys:Great to see you, Gladys! Samantha:Hail, Samantha! Well met!]

本Go教程到此结束!(译者注:还有一总结篇)

添加一个测试

本文翻译自《Add a test》。

现在你已经将代码放到了一个稳定的位置(顺便说一句,做得很好),接下来添加一个测试。在开发期间测试你的代码,可以发现更改代码造成的Bug。在本文中,你将为Hello函数添加一个测试。

注意:本主题是从《创建一个Go模块》开始的多部分教程的一部分。

Go对单元测试的内置支持使你可以轻松地进行测试。具体来说,使用命名约定、Go的testinggo test命令,你可以快速编写和进行测试。

1 在greetings目录中,创建一个名为greetings_test.go的文件。

以_test.go结尾的文件名告诉go test命令该文件包含测试函数。

2 在greetings_test.go中,粘贴以下代码并保存文件。

package greetings

import (
    "testing"
    "regexp"
)

// TestHelloName函数使用一个名字name调用greetings.Hello函数,检查返回值是否有效。
func TestHelloName(t *testing.T) {
    name := "Gladys"
    want := regexp.MustCompile(`\b`+name+`\b`)
    msg, err := Hello("Gladys")
    if !want.MatchString(msg) || err != nil {
        t.Fatalf(`Hello("Gladys") = %q, %v, want match for %#q, nil`, msg, err, want)
    }
}

// TestHelloEmpty函数传入空字符串调用greetings.Hello函数,检查是否会发生错误。
func TestHelloEmpty(t *testing.T) {
    msg, err := Hello("")
    if msg != "" || err == nil {
        t.Fatalf(`Hello("") = %q, %v, want "", error`, msg, err)
    }
}

在此代码中,你:

  • 在与被测代码相同的包中实现测试函数。
  • 创建两个测试函数来测试greetings.Hello函数。测试函数的名字的形式为TestName,其中Name表示有关特定测试的信息。此外,测试函数将指针指向testing包的testing.T类型作为参数。你可以使用此类型的方法在测试中进行报告和记录各种信息。
  • 实现两个测试函数:

TestHelloName函数调用Hello函数,传递一个name值,应该能够返回有效的响应消息。如果返回一个错误或一个意外消息(不包含你传入的name值),你可以使用t参数的Fatalf方法将消息打印到控制台并结束执行。

TestHelloEmpty函数使用空字符串调用Hello函数。此测试旨在确认你的错误处理是否有效。如果返回非空字符串或没有错误,则使用t参数的Fatalf方法将消息打印到控制台并结束执行。

3 在greetings目录下命令行运行go test命令执行测试。

go test命令执行测试文件(名称以_test.go结尾)中的测试函数(名称以Test开头)。你可以添加-v标志以获得所有测试及其结果的详细输出。

本测试应该可以通过。

$ go test
PASS
ok      example.com/greetings   0.364s

$ go test -v
=== RUN   TestHelloName
--- PASS: TestHelloName (0.00s)
=== RUN   TestHelloEmpty
--- PASS: TestHelloEmpty (0.00s)
PASS
ok      example.com/greetings   0.372s

4 破坏greetings.Hello函数以查看失败的测试结果。

测试TestHelloName函数检查你传递name参数给Hello函的返回值。要查看失败的测试结果,请更改greetings.Hello函数,使返回值中不再包含name参数的值。

在greetings/greetings.go中,粘贴以下代码代替Hello函数。请注意,突出显示的行更改该函数返回的值,就好像name参数被意外删除一样。

// Hello函数返回对指定名字的人员的一句问候语。
func Hello(name string) (string, error) {
    // 如果name参数的值为空字符串,返回一个错误信息。
    if name == "" {
        return name, errors.New("empty name")
    }
    // 使用一个随机格式创建一个消息。
    // message := fmt.Sprintf(randomFormat(), name)
    message := fmt.Sprint(randomFormat())
    return message, nil
}

5 在greetings目录下命令行运行go test命令执行测试。

这次,在没有-v标志的情况下运行go test。输出将仅包含失败测试的结果,这在你有大量测试时很有用。测试TestHelloName函数应该会失败——测试TestHelloEmpty函数仍然会通过。

$ go test
--- FAIL: TestHelloName (0.00s)
    greetings_test.go:15: Hello("Gladys") = "Hail, %v! Well met!", <nil>, want match for `\bGladys\b`, nil
FAIL
exit status 1
FAIL    example.com/greetings   0.182s

在下一个(也是最后一个)主题中,你将了解如何编译和安装代码以在本地运行它。

为多个人返回问候语

本文翻译自《Return greetings for multiple people》。

在你对模块代码所做的最后更改中,你将添加代码,在一个请求中获取对多个人的问候语。换句话说,你将处理多个值输入,然后对应地输出多个值。为此,你需要将一组名字传递给一个可以为每个名字返回一句问候语的函数。

注意:本主题是从《创建一个Go模块》开始的多部分教程的一部分。

但有一个问题。将Hello函数的参数从单个名字name更改为一组名字将会改变函数的签名。如果你已经发布了example.com/greeting模块,并且用户已经编写了调用Hello函数的代码,那么这种更改将破坏他们的程序。

在这种情况下,更好的选择是编写一个具有不同名称的新函数。新函数将采用多个参数。这保留了旧功能以实现向后兼容性。

1 在greetings/greetings.go中,更改你的代码,使其如下所示。

package greetings

import (
    "errors"
    "fmt"
    "math/rand"
    "time"
)

// Hello函数为给定名字的人返回一句问候语。
func Hello(name string) (string, error) {
    // 如果name参数的值为空字符串,那么返回一条错误信息。
    if name == "" {
        return name, errors.New("empty name")
    }
    // 使用一个随机格式创建一条消息。
    message := fmt.Sprintf(randomFormat(), name)
    return message, nil
}

// Hellos函数返回一个映射map,用来关联每个给出名字的人和发给他的一句问候语消息。
func Hellos(names []string) (map[string]string, error) {
    // 一个映射map,用来关联名字和消息。
    messages := make(map[string]string)
    // 循环遍历接收到的names切片,为其中每个名字调用Hello函数获取一条消息。
    for _, name := range names {
        message, err := Hello(name)
        if err != nil {
            return nil, err
        }
        //在这个映射map中,关联名字和对应的消息。
        messages[name] = message
    }
    return messages, nil
}

// 初始化随机数种子
func init() {
    rand.Seed(time.Now().UnixNano())
}

// randomFormat函数返回多个问候语消息中随机选择的其中一个。
func randomFormat() string {
    // 一个消息格式切片。
    formats := []string{
        "Hi, %v. Welcome!",
        "Great to see you, %v!",
        "Hail, %v! Well met!",
    }

    // 返回一个随机选择的消息格式。
    return formats[rand.Intn(len(formats))]
}
  • 添加一个Hellos函数,其参数是一组名字而不是单个名字。此外,你将其返回类型之一从字符串更改为映射,以便你可以返回名字到问候语消息的映射。
  • 让新的Hellos函数调用现有的Hello函数。这有助于减少重复,同时保留这两个函数。
  • 创建一个消息messages映射,将每个接收到的名字(作为关键字)与生成的消息(作为值)相关联。在Go中,使用以下语法初始化映射:make(map[key-type]value-type)。让Hellos函数将此映射返回给调用者。有关映射类型map的更多信息,请参阅Go博客上的实践Go map
  • 遍历你的函数收到的名字,检查每个名字是否具有非空值,然后将消息与每个名字相关联。在此for循环中,range返回两个值:循环中当前条目的索引和条目值的一个副本。你不需要索引,因此你使用Go空白标识符(一个下划线)来忽略它。有关更多信息,请参阅Effective Go中的空白标识符

2 在你的hello/hello.go的代码调用中,传递一个名字切片,然后打印输出你获得的名字/消息映射的内容。

package main

import (
    "fmt"
    "log"

    "example.com/greetings"
)

func main() {
    log.SetPrefix("greetings: ")
log.SetFlags(0)

// 一个名字切片
names := []string{"Gladys", "Samantha", "Darrin"}

	// 为names请求响应的问候语消息
    messages, err := greetings.Hellos(names)
    if err != nil {
        log.Fatal(err)
}

fmt.Println(messages)
}

通过这些更改,你可以:

  • 创建一个names切片类型变量,包含三个名字。
  • names变量作为参数传递给Hellos函数。

3 在命令行中,切换到包含hello/hello.go的目录,然后使用go un运行并确认代码是否有效

输出应该是将名字与消息关联起来的映射的字符串表示,如下所示:

$ go run .
map[Darrin:Hail, Darrin! Well met! Gladys:Hi, Gladys. Welcome! Samantha:Hail, Samantha! Well met!]

本主题介绍了表示名/值对的映射类型。它还引入了通过为模块中的新功能或更改功能来实现新功能并保持向后兼容性的思想。有关向后兼容性的详细信息,请参阅保持模块兼容

接下来,你将使用Go内置的功能为代码创建一个单元测试

随机返回一句问候语

本文翻译自《Return a random greeting》。

在本节中,你将更改你的代码,以便它不会每次都返回同一句问候语,而是返回预定义的几句问候语之一。

注意:本主题是从《创建一个Go模块》开始的多部分教程的一部分。

为此,你将使用Go切片(slice)。切片类似于数组,不同之处在于它的大小会随着你添加和删除元素条目而动态变化。切片是Go中最有用的类型之一。

你将添加一个小切片以包含三句问候语,然后让你的代码随机返回其中一句。有关切片的更多信息,请参阅Go博客中的Go切片

1 在greetings/greetings.go中,更改你的代码,使其如下所示。

package greetings

import (
    "errors"
    "fmt"
    "math/rand"
    "time"
)

// Hello函数对给出名字name的人员返回一句问候语。
func Hello(name string) (string, error) {
    // 如果没有给出名字,返回一条错误消息。
    if name == "" {
        return name, errors.New("empty name")
    }
    // 使用一个随机格式创建一条消息。
    message := fmt.Sprintf(randomFormat(), name)
    return message, nil
}

// 使用时间戳初始化随机数种子。
func init() {
    rand.Seed(time.Now().UnixNano())
}

// randomFormat函数返回一组问候语中的一句。返回的问候语是随机选择的。func randomFormat() string {
    // 一个存储消息格式的切片。
    formats := []string{
        "Hi, %v. Welcome!",
        "Great to see you, %v!",
        "Hail, %v! Well met!",
    }

    // 通过为消息格式切片指定随机索引,返回随机选择的消息格式。
    return formats[rand.Intn(len(formats))]
}

在此代码中,你:

  • 添加一个randomFormat函数,该函数返回随机选择的问候语消息格式。请注意,randomFormat以小写字母开头,使其只能在其自身包中的被访问(换句话说,它不会被导出给其他包调用)。
  • randomFormat函数中声明了包含三种消息格式的切片。声明切片时,在方括号中省略其大小,如下所示:[]string。这告诉Go,切片下的数组大小可以动态更改。
  • 使用math/rand生成一个随机数,用于从切片中选择一个条目。
  • 添加一个init函数以使用当前时间戳为rand包生成随机数种子。Go在程序启动时,在全局变量初始化之后,自动执行init函数。有关init函数的更多信息,请参阅Effective Go
  • Hello函数中,调用randomFormat函数来获取你将返回的消息的格式,然后使用该格式和name参数的值一起创建一句问候语消息。
  • 像以前一样返回消息(或错误)。

2 在hello/hello.go中,更改你的代码,使其如下所示。

你将Gladys的名字(或其他名字,如果你愿意)作为参数添加到hello.go中的Hello函数调用。

package main

import (
    "fmt"
    "log"

    "example.com/greetings"
)

func main() {
    // 设置内置的日志记录器的属性,包括设置日志条目的前缀,设置标志0以不输出日志条目的时间、源文件名称和行号。
    log.SetPrefix("greetings: ")
    log.SetFlags(0)

    // 请求一句问候语信息。
    message, err := greetings.Hello("Gladys")
    // 如果返回一个错误,将其打印到控制台并退出程序。
    if err != nil {
        log.Fatal(err)
    }

    // 如果没有返回错误,将返回的消息打印到控制台。
    fmt.Println(message)
}

3 在命令行的hello目录中,运行hello.go以确认代码有效。多次运行它,注意问候语发生了变化。

$ go run .
Great to see you, Gladys!

$ go run .
Hi, Gladys. Welcome!

$ go run .
Hail, Gladys! Well met!

接下来,你将使用一个切片来问候多个人