作为 Write for DOnations 计划的一部分,作者选择了 Diversity in Tech Fund 来接受捐赠。
介绍
在 1.13 版本中,Go 的作者添加了一种管理 Go 项目所依赖的库的新方法,称为 Go 模块 。 添加 Go 模块是为了满足日益增长的需求,使开发人员更容易维护各种版本的依赖项,并为开发人员在计算机上组织项目的方式增加了更大的灵活性。 Go 模块通常由一个项目或库组成,并包含一组 Go 包,然后一起发布。 Go 模块解决了原始系统 GOPATH 的许多问题,它允许用户将他们的项目代码放在他们选择的目录中,并为每个模块指定依赖项的版本。
在本教程中,您将创建自己的公共 Go 模块并将包添加到新模块。 此外,您还将其他人的公共模块添加到您自己的项目中,以及将该模块的特定版本添加到您的项目中。
先决条件
要遵循本教程,您将需要:
- Go version 1.16 or greater installed. To set this up, follow the How To Install Go tutorial for your operating system.
- 熟悉用 Go 编写包。 To learn more, follow the How To Write Packages in Go tutorial.
创建新模块
乍一看,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
创建模块时,此命令将返回以下输出:
Outputgo: 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
内容将与此类似,这并不多:
项目/mymodule/go.mod
module mymodule go 1.16
第一行,module
指令,告诉 Go 模块的名称,以便当它查看包中的 import
路径时,它知道不要在其他地方查找 mymodule
. mymodule
值来自您传递给 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
功能:
项目/mymodule/main.go
package main import "fmt" func main() { fmt.Println("Hello, Modules!") }
在 Go 中,每个目录都被认为是自己的包,每个文件都有自己的 package
声明行。 在刚刚创建的main.go
文件中,package
被命名为main
。 通常,您可以随意命名包,但 main
包在 Go 中是特殊的。 当 Go 看到一个包被命名为 main
时,它知道该包应该被视为二进制文件,并且应该被编译为可执行文件,而不是设计用于其他程序的库。
在定义 package
之后,import
声明说要导入 fmt 包,以便您可以使用它的 Println
函数来打印 [ X153X] 消息到屏幕上。
最后定义了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!
文本:
OutputHello, Modules!
在本节中,您向模块添加了一个 main.go
文件,其中包含一个初始 main
函数,该函数打印 Hello, Modules!
。 在这一点上,你的程序还没有从 Go 模块中受益——它可能是你计算机上任何地方使用 go run
运行的文件。 Go 模块的第一个真正好处是能够将依赖项添加到任何目录中的项目中,而不仅仅是 GOPATH
目录结构。 您还可以将包添加到模块中。 在下一部分中,您将通过在其中创建一个附加包来扩展您的模块。
将包添加到您的模块
与标准 Go 包类似,一个模块可能包含任意数量的包和子包,也可能根本不包含。 对于此示例,您将在 mymodule
目录中创建一个名为 mypackage
的包。
通过在 mymodule
目录中使用 mypackage
参数运行 mkdir
命令来创建这个新包:
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!
:
项目/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
包中的 import
来使用它。 这类似于您导入其他包的方式,例如之前的 fmt
包,只是这次您将在导入路径的开头包含模块的名称。 从 mymodule
目录中打开您的 main.go
文件,并通过添加以下突出显示的行来添加对 PrintHello
的调用:
项目/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
。
像以前一样使用 mymodule
目录中的 go run
和 main.go
运行更新的模块:
go run main.go
当您再次运行该模块时,您将看到之前的 Hello, Modules!
消息以及从新 mypackage
的 PrintHello
函数打印的新消息:
OutputHello, Modules! Hello, Modules! This is mypackage speaking!
现在,您已经通过使用 PrintHello
函数创建名为 mypackage
的目录,将新包添加到初始模块。 但是,随着您的模块功能的扩展,开始使用您自己的其他人的模块可能会很有用。 在下一节中,您将添加一个远程模块作为您的依赖项。
添加远程模块作为依赖项
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 工具的版本而改变,但更改的整体结构应该是相似的:
项目/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
指令也会包含 // indirect
注释。 这条评论说,在添加 require
指令时,模块的任何源文件中都没有直接引用该模块。 文件中还添加了一些额外的 require
行。 这些行是 Cobra 所依赖的其他模块,确定的 Go 工具也应该被引用。
您可能还注意到在运行 go run
命令后在 mymodule
目录中创建了一个新文件 go.sum
。 这是 Go 模块的另一个重要文件,包含 Go 用来记录特定哈希和依赖项版本的信息。 这确保了依赖关系的一致性,即使它们安装在不同的机器上。
下载依赖项后,您需要使用一些最小的 Cobra 代码更新 main.go
文件以使用新的依赖项。 使用下面的 Cobra 代码更新 mymodule
目录中的 main.go
文件,以使用新的依赖项:
项目/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
结构,其中包含一个包含现有“Hello”语句的 Run
函数,然后将通过调用 cmd.Execute()
来执行该结构。 现在,运行更新的代码:
go run main.go
您将看到以下输出,与您之前看到的类似。 不过,这一次,它使用了您的新依赖项,如 Calling cmd.Execute()!
行所示:
OutputCalling cmd.Execute()! Hello, Modules! Hello, Modules! This is mypackage speaking!
使用 go get
添加最新版本的远程依赖项,例如此处的 github.com/sp13/cobra
,可以更轻松地使用最新的错误修复来更新依赖项。 但是,有时您可能更愿意使用特定版本的模块、存储库标记或存储库分支。 在下一节中,您将在需要该选项时使用 go get
来引用这些版本。
使用特定版本的模块
由于 Go 模块是从版本控制存储库分发的,因此它们可以使用版本控制功能,例如标签、分支甚至提交。 您可以使用模块路径末尾的 @
符号以及您想要使用的版本在您的依赖项中引用这些。 早些时候,当您安装最新版本的 Cobra 时,您正在利用此功能,但您无需将其显式添加到您的命令中。 go
工具知道,如果没有使用 @
提供特定版本,它应该使用特殊版本 latest
。 latest
版本实际上不在存储库中,例如 my-tag
或 my-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/cobra
的提交 07445ea
,因为它有一些您需要的更改,并且由于某种原因您不能使用其他版本。 在这种情况下,您可以在 @
符号之后提供提交哈希,就像为分支或标签提供的一样。 在你的mymodule
目录下运行go get
命令,下载新版本:
go get github.com/spf13/cobra@07445ea
如果您再次打开模块的 go.mod
文件,您将看到 go get
已更新 github.com/spf13/cobra
的 require
行以引用您指定的提交:
项目/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/cobra
的 require
行以引用您提供的版本:
项目/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,请使用模块路径和 latest
版本再次运行 go get
:
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 模块的非常详细的技术参考。
本教程也是 DigitalOcean How to Code in Go 系列的一部分。 该系列涵盖了许多 Go 主题,从第一次安装 Go 到如何使用语言本身。