开发和发布模块

本文翻译自《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参考

总结

本文翻译自《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!

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

返回并处理一个错误

本文翻译自《Return and handle an error》。

能处理错误是可靠代码的基本特征。在本节中,你将添加一些代码以从greetings模块返回一个错误,然后在调用方处理它。

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

1 在greetings/greetings.go中,添加下面突出显示的代码。

如果你不知道该问候谁,回复一句问候语就没有意义。如果name为空,则向调用者返回一个错误。将以下代码复制到greetings.go并保存文件。

package greetings

import (
    "errors"
    "fmt"
)

// Hello函数向给出名字的人返回一句问候语。
func Hello(name string) (string, error) {
    // 如果名字没有给出,就返回一个错误信息。
    if name == "" {
        return "", errors.New("empty name")
    }

    // 如果给出了名字,就返回一句嵌入了该名字的问候语。
    // in a greeting message.
    message := fmt.Sprintf("Hi, %v. Welcome!", name)
    return message, nil
}

在此代码中,你:

  • 更改了Hello函数的代码,使其返回两个值:一个字符串和一个错误。你的调用者将检查第二个值以查看是否发生了错误。(任何Go函数都可以返回多个值。有关更多信息,请参阅Effective Go。)
  • 添加一个if语句来检查无效请求(name为空字符串),如果请求无效则返回一个错误。errors.New函数返回一个错误,其中包含错误信息。
  • 在成功返回中添加nil(表示没有错误)作为第二个值。这样,调用者就可以看到函数成功返回了。

2 在你的hello/hello.go文件中,处理Hello函数返回的错误以及非错误值。 将以下代码粘贴到hello.go中。

package main

import (
    "fmt"
    "log"

    "example.com/greetings"
)

func main() {
    // 设置预定义的日志记录器的属性,包括设置日志条目的前缀,设置标志0以禁用打印时间、源文件和行号。
    log.SetPrefix("greetings: ")
    log.SetFlags(0)

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

    // 如果没有返回错误,将返回的消息打印到控制台。
    fmt.Println(message)
}
  • 配置log以在其日志消息的开头打印命令名称 (“greetings: “),不带时间戳或源文件信息。
  • Hello函数的两个返回值(包括错误)分配给变量。
  • Hello函数参数从具体的名称更改为空字符串,以便你可以尝试运行错误处理代码。
  • 查找非零值错误。在这种情况下继续下去是没有意义的。
  • 使用标准库log包中的函数输出错误信息。如果出现错误,则使用log包的Fatal函数打印错误信息并停止程序。

3 在hello目录的命令行中,运行hello.go以确认代码有效。

现在你传递的是一个空名称,你将收到一个错误。

$ go run .
greetings: empty name
exit status 1

这是Go中的常见的错误处理方式:将错误作为值返回,以便调用者可以检查它。

接下来,你将使用Go切片随机返回选择的一句问候语

从另一个模块调用你的函数

本文翻译自《Call your code from another module》。

上一节中,你创建了一个greetings模块。在本节中,你将编写代码来调用刚刚编写的模块中的Hello函数。你将编写可作为应用程序执行的代码,并调用greetings模块中的代码。

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

1 为你的Go模块源代码创建一个hello目录。这是你将编写调用者的地方。

创建此目录后,你应该在目录层次结构中的同一级别拥有hello目录和greetings目录,如下所示:

<home>/
 |-- greetings/
 |-- hello/

例如,如果你的命令提示符位于greetings目录中,你可以使用以下命令:

cd ..
mkdir hello
cd hello

2 为你将要编写的代码启用依赖项跟踪。

要为你的代码启用依赖项跟踪,请运行go mod init命令,指定你的代码所在模块的名称。

出于本教程的目的,使用example.com/hello作为模块路径。

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

3 在你的文本编辑器中,在hello目录中,创建一个用于编写代码的文件,并将其命名为hello.go。

4 编写代码调用Hello函数,然后打印函数的返回值。 为此,将以下代码粘贴到hello.go中。

package main

import (
    "fmt"

    "example.com/greetings"
)

func main() {
    // 获取一句问候语并打印输出它。
    message := greetings.Hello("Gladys")
    fmt.Println(message)
}

在此代码中,你:

  • 声明一个main包。在Go中,作为应用程序执行的代码必须在main包中。
  • 导入两个包:example.com/greetingsfmt。这使你的代码可以访问这些包中的函数。导入example.com/greetings(你之前创建的模块中包含的包)可以让你访问Hello函数。你还导入fmt,具有处理输入和输出文本的功能(例如将文本打印到控制台)。
  • 通过调用greetings包的Hello函数获取一句问候语。

5 编辑example.com/hello模块以使用本地example.com/greetings模块。

对于生产环境,你将从代码仓库中发布example.com/greetings模块(具有反映其发布位置的模块路径),Go工具可以在其中找到它并进行下载。现在,因为你还没有发布该模块,所以你需要调整example.com/hello模块,以便它可以在你的本地文件系统上找到example.com/greetings代码。

为此,请使用go mod edit命令编辑example.com/hello模块,将Go工具从其模块路径(模块不在的位置)重定向到本地目录(它所在的位置)。

5.1 从hello目录中的命令行提示符运行以下命令:

$ go mod edit -replace example.com/greetings=../greetings

该命令指定example.com/greetings应替换为../greetings以定位依赖项。运行该命令后,hello目录中的go.mod文件应包含一个replace指令

module example.com/hello

go 1.16

replace example.com/greetings => ../greetings

5.2 从hello目录中的命令行提示符运行go mod tidy命令以同步example.com/hello模块的依赖项,添加代码所需但尚未在模块中跟踪的依赖项。

$ go mod tidy
go: found example.com/greetings in example.com/greetings v0.0.0-00010101000000-000000000000

命令完成后,example.com/hello模块的go.mod文件应该如下所示:

module example.com/hello

go 1.16

replace example.com/greetings => ../greetings

require example.com/greetings v0.0.0-00010101000000-000000000000

该命令在greetings目录中找到本地代码,然后添加require指令以指定example.com/hello需求example.com/greetings。当你在hello.go中导入greetings包时,你就创建了这个依赖项。

模块路径后面的数字是一个伪版本号(pseudo-version number)——一个生成的数字,用来代替语义版本号(该模块目前还没有)。

要引用已发布的模块,go.mod文件通常会省略replace指令并使用末尾带有标签版本号的require指令。

require example.com/greetings v1.1.0

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

6 在hello目录中的命令行提示符下,运行你的代码以确认它是否有效。

$ go run .
Hi, Gladys. Welcome!

恭喜!你已经编写了两个功能模块。

在下一主题中,你将添加一些错误处理