41 How to Use Go Modules

如何使用 Go 模块 #

简介 #

在 1.13 版本,Go 的作者增加了一种管理 Go 项目依赖库的新方法,称之为 Go 模块。Go 模块的加入是为了满足日益增长的需求,使开发者更容易维护其依赖的各种版本,同时也为开发者在计算机上组织项目的方式上,增加了更多的灵活性。Go 模块通常由一个项目或库组成,并包含一系列的 Go 包,然后一起发布。Go 模块解决了原始系统 GOPATH 的许多问题,它允许用户将项目代码放在他们选择的目录中,并为每个模块指定依赖的版本。

在本教程中,你将创建你自己的公共 Go module,并为你的新模块添加一个包。此外,你还将把别人的公共模块添加到你自己的项目中,以及把该模块的一个特定版本添加到你的项目中。

前期准备 #

要遵循本教程,你将需要:

  • 安装1.16 或更高版本的 Go。如何安装 Go ,请根据你的操作系统遵循 如何安装Go 教程。
  • 熟悉用 Go 编写软件包。要了解更多,请遵循 如何用 Go 编写包 教程。

创建一个新的模块 #

乍看之下,Go模块与 Go包 有些相似。一个模块有许多实现包的功能的 Go 代码文件,但它在根部还有两个额外的重要文件:go.mod 文件和 go.sum 文件。这些文件包含了 go 工具用来跟踪你的模块配置的信息,通常由工具维护,所以你不需要维护。

首先要做的是决定模块所处的目录。随着 Go 模块的引入,Go 项目有可能位于文件系统的任何地方,而不仅仅是 Go 定义的特定目录。你可能已经有了一个存放项目的目录,但在本教程中,你将创建一个名为 projects 的目录,新模块将被称为 mymodule。你可以通过 IDE 或者命令行来创建 projects 目录。

如果你使用的是命令行,首先新建 projects目录并进入其中:

mkdir projects
cd projects

接下来,你将创建模块目录本身。通常,模块的顶层目录名称与模块名称相同,这使得模块配置的信息更容易被追踪。在你的 projects 目录下,运行以下命令来创建 mymodule 目录:

mkdir mymodule

一旦你创建了模块目录,目录结构将看起来像这样:

└── projects
    └── mymodule

下一步是在 mymodule 目录下创建一个 go.mod 文件来定义 Go 模块本身。要做到这一点,你要使用 go 工具的 mod init 命令,并向它提供模块的名称,在这个例子中是 mymodule。现在通过在 mymodule 目录下运行 go mod init 来创建模块,并向它提供模块的名称 mymodule

go mod init mymodule

该命令在创建模块时将返回以下输出:

Output
go: creating new go.mod: module mymodule

随着模块的创建,你的目录结构现在看起来会是这样:

└── projects
    └── mymodule
        └── go.mod

现在你已经创建了一个模块,让我们看一下 go.mod 文件的内容,看看 go mod init 命令做了什么。

了解 go.mod 文件 #

当你用 go 工具运行命令时,go.mod 文件是一个非常重要的部分。它是包含模块名称和你自己模块所依赖的其他模块的版本的文件。它还可以包含其他指令,如 replace,这对同时进行多个模块的开发很有帮助。

mymodule 目录下,用 nano 或你喜欢的文本编辑器打开 go.mod 文件:

nano go.mod

内容将看起来与此类似,这并不重要:

projects/mymodule/go.mod

module mymodule

go 1.16

第一行,module 指令,告诉 Go 你的模块名称,这样当它在包中寻找 import 路径时,它就不会在其他地方寻找 mymodulemymodule 的值来自你传递给 go mod init 的参数:

module mymodule

文件中的另外一行,即 go 指令,告诉 Go 模块所针对的语言版本。在本例中,由于该模块是用 Go 1.16 创建的,所以 go 指令显示为 1.16

go 1.16

随着更多的信息被添加到模块中,这个文件将被扩展,但现在看看它是个好主意,看看它是如何随着依赖关系的进一步添加而变化的。

现在你已经用 go mod init 创建了一个 Go 模块,并查看了初始 go.mod 文件的内容,但你的模块还没有做任何事情。现在是时候让你的模块更进一步,添加一些代码了。

在你的模块中添加Go代码 #

为了确保模块被正确创建,并添加代码以便你能运行你的第一个 Go 模块,你将在 mymodule 目录下创建一个 main.go 文件。main.go 文件在 Go 程序中通常用来表示程序的起始点。该文件的名称并不像里面的 main 函数那么重要,但两者的匹配使其更容易被找到。在本教程中,main 函数在运行时将打印出 Hello, Modules!

要创建这个文件,用 nano 或你喜欢的文本编辑器打开 main.go 文件:

nano main.go

main.go 文件中,添加以下代码来定义你的 main 包,导入 fmt 包,然后在 main 函数中打印出 Hello, Modules!信息:

projects/mymodule/main.go

package main

import "fmt"

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

在 Go 中,每个目录都被视为自己的包,每个文件都有自己的 package 声明行。在你刚刚创建的 main.go 文件中, 被命名为 main。通常情况下,你可以以任何方式命名包,但 main 包在 Go 中是很特别的。当 Go 看到一个包被命名为 main 时,它知道这个包应该被视为二进制文件,应该被编译成可执行文件,而不是一个旨在用于其他程序的库。

在定义了 之后,import 声明说要导入 fmt 包,所以你可以使用它的 Println 函数将 "Hello, Modules!" 信息打印到屏幕上。

最后,定义了 main 函数。main 函数是 Go 的另一个特例,与main包有关。当 Go 看到一个名为 main 的包内有一个名为 main 的函数时,它知道 main 函数是它应该运行的第一个函数。这被称为程序的入口点。

一旦你创建了 main.go 文件,该模块的目录结构将类似于这样:

└── projects
    └── mymodule
        └── go.mod
        └── main.go

如果你熟悉使用 Go 和 GOPATH ,运行模块中的代码就类似于从 GOPATH 中的一个目录中进行。(如果你不熟悉 GOPATH 也不用担心,因为使用模块可以代替它的使用。)

在 Go 中运行可执行程序有两种常见的方法:用 go build 构建二进制文件或用 go run 运行文件。在本教程中,你将使用 go run 直接运行模块,而不是构建二进制文件,后者必须单独运行。

go run 运行你创建的 main.go 文件:

go run main.go

运行该命令将打印代码中定义的 Hello, Modules! 文本:

Output
Hello, Modules!

在这一节中,你为你的模块添加了一个 main.go 文件,其中的初始 main 函数打印出 Hello, Modules!。在这一点上,你的程序还没有从 Go 模块中获益—它可以是你电脑上任何地方的一个文件,用 go run 来运行。Go 模块的第一个真正好处是能够在任何目录下为你的项目添加依赖,而不仅仅是 GOPATH 目录结构。你还可以向你的模块添加包。在下一节中,你将通过在模块中创建一个额外的包来扩展你的模块。

为你的模块添加一个包 #

类似于标准的 Go 包,一个模块可以包含任何数量的包和子包,也可以完全不包含。在这个例子中,你将在 mymodule 目录下创建一个名为 mypackage 的包。

通过在 mymodule 目录下运行 mkdir 命令并加上 mypackage 参数来创建这个新包:

mkdir mypackage

这将创建新的目录 mypackage 作为 mymodule 目录的一个子包:

└── projects
    └── mymodule
        └── mypackage
        └── main.go
        └── go.mod

使用 cd 命令将当前目录改为新的 mypackage 目录,然后使用 nano ,或你喜欢的文本编辑器,创建一个 mypackage.go 文件。这个文件可以有任何名字,但使用与软件包相同的名字可以更容易找到软件包的主文件:

cd mypackage
nano mypackage.go

mypackage.go 文件中,添加一个名为 PrintHello 的函数,当被调用时,将打印 Hello, Modules! This is mypackage speaking!

projects/mymodule/mypackage/mypackage.go

package mypackage

import "fmt"

func PrintHello() {
	fmt.Println("Hello, Modules! This is mypackage speaking!")
}

由于你希望 PrintHello 函数可以从另一个包中使用,函数名称中的大写字母 P 很重要。大写字母意味着该函数是导出的,对任何外部程序都是可用的。关于 Go 中包的可见性的更多信息,Understanding Package Visibility in Go 包括更多细节。

现在你已经创建了带有导出函数的 mypackage 包,你将需要从 mymodule 包中 导入 它来使用它。这与你导入其他包的方法类似,比如之前的 fmt 包,只是这次你要在导入路径的开头加入你的模块名称。从 mymodule 目录下打开你的 main.go 文件,通过添加下面高亮的行来添加对 PrintHello 的调用:

projects/mymodule/main.go

package main

import (
	"fmt"

	"mymodule/mypackage"
)

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

	mypackage.PrintHello()
}

如果你仔细看一下 import 语句,你会看到新的导入以 mymodule 开始,也就是你在 go.mod 文件中设置的模块名称。后面是路径分隔符和你要导入的包,本例中是mypackage

"mymodule/mypackage"

在未来,如果你在 mypackage 内添加包,你也会以类似的方式将它们添加到导入路径的末尾。例如,如果你在 mypackage 内有另一个叫 extrapackage 的包,你对该包的导入路径将是 mymodule/mypackage/extrapackage

像以前一样,用go runmain.gomymodule 目录中运行你的更新模块:

go run main.go

当你再次运行该模块时,你会看到先前的 “Hello, Modules!“信息,以及新的 mypackagePrintHello 函数打印的新信息:

Output
Hello, Modules!
Hello, Modules! This is mypackage speaking!

你现在通过创建一个名为 mypackage 的目录和 PrintHello 函数,为你的初始模块添加了一个新包。随着你的模块功能的扩展,在你的模块中开始使用其他人的模块可能会很有用。在下一节中,你将添加一个远程模块作为你的模块的依赖。

添加一个远程模块作为依赖 #

Go 模块是通过版本控制库(通常是Git库)发布的。当你想添加一个新的模块作为自己的依赖时,你会使用仓库的路径作为你想使用的模块的引用方式。当 Go 看到这些模块的导入路径时,它可以根据这个仓库的路径推断出在哪里可以远程找到它。

在这个例子中,你要在你的模块中加入对 github.com/spf13/cobra 库的依赖。Cobra 是一个常用来创建控制台应用程序的库,但我们不会在本教程中讨论这个问题。

与你创建 mymodule 模块时类似,你将再次使用 go 工具。然而,这一次,你将在 mymodule 目录下运行 go get 命令。运行 go get 并提供你想添加的模块。在这种情况下,你会得到 github.com/spf13/cobra

go get github.com/spf13/cobra

当你运行这个命令时,go 工具将从你指定的路径查找 Cobra 仓库,并通过查看仓库的分支和标签来确定哪个版本的 Cobra 是最新的。然后,它将下载该版本,并通过在 go.mod 文件中添加模块名称和版本来跟踪它所选择的版本,以供将来参考。

现在,打开 mymodule 目录下的 go.mod 文件,看看当你添加新的依赖时,go 工具如何更新 go.mod 文件。下面的例子可能会有变化,这取决于当前已经发布的 Cobra 版本或你使用的 Go 工具的版本,但整体的变化结构应该是相似的:

projects/mymodule/go.mod

module mymodule

go 1.16

require (
	github.com/inconshreveable/mousetrap v1.0.0 // indirect
	github.com/spf13/cobra v1.2.1 // indirect
	github.com/spf13/pflag v1.0.5 // indirect
)

增加了一个使用 require 指令的新部分。这个指令告诉 Go 你想要哪个模块,比如 github.com/spf13/cobra,以及你添加的模块的版本。有时 require 指令也会包括一个//间接注释。这个注释表示,在添加 require 指令时,该模块没有在该模块的任何源文件中被直接引用。文件中还增加了一些额外的 require 行。这些行是 Cobra 所依赖的其他模块,Go 工具认为这些模块也应该被引用。

你可能还注意到,在运行 go run 命令后,在 mymodule 目录下创建了一个新的文件,go.sum 。这是 Go 模块的另一个重要文件,包含了 Go 用来记录依赖关系的具体哈希值和版本的信息。这确保了依赖关系的一致性,即使它们被安装在不同的机器上。

一旦你下载了依赖关系,你要用一些最小的 Cobra 代码更新你的 main.go 文件,以使用新的依赖关系。用下面的Cobra 代码更新 mymodule 目录下的 main.go 文件,以使用新的依赖性:

projects/mymodule/main.go

package main

import (
	"fmt"
    
	"github.com/spf13/cobra"

	"mymodule/mypackage"
)

func main() {
	cmd := &cobra.Command{
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("Hello, Modules!")

			mypackage.PrintHello()
		},
	}

	fmt.Println("Calling cmd.Execute()!")
	cmd.Execute()
}

这段代码创建了一个 cobra.Command 结构,其中的 Run 函数包含你现有的 “Hello” 语句,然后将通过调用 cmd.Execute() 来执行。现在,运行更新后的代码:

go run main.go

你会看到下面的输出,它看起来与你之前看到的相似。不过这一次,它使用了你的新的依赖关系,如 Calling cmd.Execute()! 一行所示:

Output
Calling cmd.Execute()!
Hello, Modules!
Hello, Modules! This is mypackage speaking!

使用 go get 来添加远程依赖的最新版本,例如这里的 github.com/sp13/cobra ,使你的依赖更容易保持最新的错误修复。然而,有时你可能更愿意使用一个特定版本的模块、一个版本库标签或一个版本库分支。在下一节中,你将使用 go get 来引用这些版本,当你想要这个选项时。

使用一个模块的特定版本 #

由于 Go 模块是从版本控制库中发布的,它们可以使用版本控制功能,如标签、分支、甚至 commits 。你可以在你的依赖关系中使用@符号在模块路径的末尾加上你想使用的版本来引用这些。早些时候,当你安装最新版本的Cobra 时,你正在利用这种能力,但你不需要在命令中明确添加它。go 工具知道,如果没有用@提供特定的版本,它应该使用特殊的版本 latestlatest 版本实际上并不在版本库中,就像 my-tagmy-branch 可能是一样。它作为一个辅助工具内置于 go 工具中,所以你不需要自己去搜索最新的版本。

例如,当你最初添加你的依赖关系时,你也可以使用下面的命令获得同样的结果:

go get github.com/spf13/cobra@latest

现在,想象有一个你使用的模块,目前正在开发中。在这个例子中,称它为 your_domain/sammy/awesome 。这个 awesome 模块正在增加一个新的功能,工作在一个叫做 new-feature 的分支中进行。要把这个分支作为你自己的模块的依赖项,你可以向 go get 提供模块的路径,后面是@符号,再后面是分支的名称:

go get your_domain/sammy/awesome@new-feature

运行这个命令会使 go 连接到 your_domain/sammy/awesome 仓库,下载 new-feature 分支的当前最新提交,并将该信息添加到 go.mod 文件。

不过,分支并不是唯一可以使用@选项的方式。这个语法可以用于标签,甚至是版本库的特定提交。例如,有时你正在使用的库的最新版本可能有一个坏的提交。在这种情况下,引用破损提交之前的提交可能会很有用。

以你的模块所依赖的 Cobra 为例,假设你需要引用 github.com/spf13/cobra07445ea 提交,因为它有一些你需要的修改,而你因为某些原因不能使用其他版本。在这种情况下,你可以在@符号后面提供提交哈希值,就像对分支或标签一样。在你的 mymodule目 录下运行go get命令,输入模块和版本来下载新版本:

go get github.com/spf13/cobra@07445ea

如果你再次打开你模块的 go.mod 文件,你会看到 go get 已经更新了 github.com/spf13/cobrarequire行,引用你指定的提交:

projects/mymodule/go.mod

module mymodule

go 1.16

require (
	github.com/inconshreveable/mousetrap v1.0.0 // indirect
	github.com/spf13/cobra v1.1.2-0.20210209210842-07445ea179fc // indirect
	github.com/spf13/pflag v1.0.5 // indirect
)

由于提交是一个特定的时间点,与标签或分支不同,Go在 require 指令中包含了额外的信息,以确保它在未来使用正确的版本。如果你仔细看一下版本,你会发现它确实包括你提供的提交散列。v1.1.2-0.20210209210842-07445ea179fc.

Go 模块也使用这个功能来支持发布不同版本的模块。当 Go 模块发布新版本时,一个新的标签会被添加到版本库中,并以版本号作为标签。如果你想使用一个特定的版本,你可以查看版本库中的标签列表,找到你要找的版本。如果你已经知道版本,你可能不需要在标签中搜索,因为版本标签的命名是一致的。

再以 Cobra 为例,假设你想使用 Cobra 1.1.1 版本。你可以查看 Cobra 仓库,发现它有一个名为 v1.1.1 的标签,还有其他标签。要使用这个标记的版本,你可以在 go get 命令中使用 @ 符号,就像你使用一个非版本标记或分支。现在,通过运行以 v1.1.1 为版本的 go get 命令,更新你的模块以使用 Cobra 1.1.1:

go get github.com/spf13/cobra@v1.1.1

现在,如果你打开你的模块的 go.mod 文件,你会看到 go get 已经更新了 github.com/spf13/cobrarequire 行,引用你提供的版本:

projects/mymodule/go.mod

module mymodule

go 1.16

require (
	github.com/inconshreveable/mousetrap v1.0.0 // indirect
	github.com/spf13/cobra v1.1.1 // indirect
	github.com/spf13/pflag v1.0.5 // indirect
)

最后,如果你正在使用一个特定版本的库,例如 07445ea 提交或早期的 v1.1.1 ,但你确定你宁愿开始使用最新版本,可以通过使用特殊的 latest 版本来实现。要将你的模块更新到最新版本的 Cobra,请再次运行 go get,输入模块路径和 latest 版本:

go get github.com/spf13/cobra@latest

一旦这个命令完成,go.mod 文件将更新为你引用特定版本的 Cobra 之前的样子。根据你的 Go 版本和当前最新的Cobra 版本,你的输出可能会略有不同,但你仍然应该看到 require 部分的 github.com/spf13/cobra 行再次更新为最新版本:

module mymodule

go 1.16

require (
	github.com/inconshreveable/mousetrap v1.0.0 // indirect
	github.com/spf13/cobra v1.2.1 // indirect
	github.com/spf13/pflag v1.0.5 // indirect
)

go get 命令是一个强大的工具,你可以用它来管理 go.mod 文件中的依赖关系,而不需要手动编辑它。正如你在本节中看到的,在模块名称中使用 @ 字符可以让你使用模块的特定版本,从发布版本到特定的版本库提交。它甚至可以用来回到你的依赖的 latest 版本。使用这些选项的组合将使你能够确保你的程序在未来的稳定性。

总结 #

在本教程中,你创建了一个带有子包的 Go 模块,并在你的模块中使用该包。你还将另一个模块作为依赖关系添加到你的模块中,并探索了如何以各种方式引用模块版本。

关于 Go 模块的更多信息,Go 项目有一系列博文介绍 Go 工具如何与模块互动和理解模块。Go 项目还在 Go模块参考 中为 Go 模块提供了非常详细的技术参考。

本教程也是 DigitalOceanHow to Code in Go 系列教程的一部分。该系列涵盖了许多 Go 主题,从首次安装 Go 到如何使用语言本身。