跳转到主要内容

介绍


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

在本教程中,您将创建自己的公共 Go 模块并将包添加到新模块。此外,您还将其他人的公共模块添加到您自己的项目中,以及将该模块的特定版本添加到您的项目中。

先决条件


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

  • 安装 1.16 或更高版本。要进行设置,请按照您的操作系统的如何安装 Go 教程进行操作。
  • 熟悉用 Go 编写包。要了解更多信息,请遵循如何在 Go 中编写包教程。


创建新模块


乍一看,Go 模块看起来类似于 Go 包。一个模块有许多实现包功能的 Go 代码文件,但它在根目录中还有两个额外且重要的文件:go.mod 文件和 go.sum 文件。这些文件包含 go 工具用于跟踪模块配置的信息,并且通常由工具维护,因此您不需要。

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

如果你使用命令行,首先创建项目目录并导航到它:

mkdir projects
cd 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 模块的名称,这样当它查看包中的导入路径时,它就知道不要在别处寻找 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 函数重要,但两者匹配更容易找到。在本教程中,主函数将打印出 Hello, Modules!运行时。

要创建文件,请使用 nano 或您喜欢的文本编辑器打开 main.go 文件:

nano main.go


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

projects/mymodule/main.go

package main

import "fmt"

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

在 Go 中,每个目录都被认为是自己的包,每个文件都有自己的包声明行。在刚刚创建的 main.go 文件中,包名为 main。通常,您可以随意命名包,但主包在 Go 中是特殊的。当 Go 看到一个名为 main 的包时,它知道该包应该被视为二进制文件,并且应该被编译成可执行文件,而不是设计用于其他程序的库。

定义包后,导入声明说要导入 fmt 包,这样您就可以使用它的 Println 函数将“Hello, Modules!”消息打印到屏幕上。

最后,定义主函数。 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 文件,其中包含一个打印 Hello, Modules! 的初始 main 函数。在这一点上,你的程序还没有从 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!这是我的包裹说话!调用时:

projects/mymodule/mypackage/mypackage.go

package mypackage

import "fmt"

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

由于您希望 PrintHello 函数可从另一个包中使用,因此函数名称中的大写 P 很重要。大写字母表示该功能已导出并可用于任何外部程序。有关 Go 中包可见性如何工作的更多信息,了解 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。

像以前一样使用 mymodule 目录中的 go run 和 main.go 运行更新的模块:

go run main.go


当您再次运行该模块时,您将看到 Hello, Modules!之前的消息以及从新 mypackage 的 PrintHello 函数打印的新消息:

Output
Hello, 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 工具的版本而改变,但更改的整体结构应该是相似的:

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 指令时,模块没有在任何模块的源文件中直接引用。文件中还添加了一些额外的要求行。这些行是 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()
}

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

go run main.go


您将看到以下输出,与您之前看到的类似。不过,这一次,它使用了您的新依赖项,如调用 cmd.Execute() 所示!线:

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


使用 go get 添加远程依赖项的最新版本,例如此处的 github.com/sp13/cobra,可以更轻松地使用最新的错误修复来更新依赖项。但是,有时您可能更愿意使用特定版本的模块、存储库标签或存储库分支。在下一节中,您将在需要该选项时使用 go get 来引用这些版本。

使用特定版本的模块


由于 Go 模块是从版本控制存储库分发的,因此它们可以使用版本控制功能,例如标签、分支甚至提交。您可以使用模块路径末尾的 @ 符号以及您想要使用的版本在您的依赖项中引用这些。早些时候,当您安装最新版本的 Cobra 时,您正在利用此功能,但您无需将其显式添加到您的命令中。 go 工具知道,如果没有使用 @ 提供特定版本,它应该使用最新的特殊版本。最新版本实际上不在存储库中,例如 my-tag 或 my-branch 可能。它作为助手内置在 go 工具中,因此您无需自己搜索最新版本。

例如,当您最初添加依赖项时,您还可以使用以下命令来获得相同的结果:

go get github.com/spf13/cobra@latest


现在,想象一下您正在使用的一个模块正在开发中。对于此示例,将其命名为 your_domain/sammy/awesome。这个很棒的模块中添加了一个新功能,并且工作正在一个名为 new-feature 的分支中完成。要将此分支添加为您自己的模块的依赖项,您需要提供 go get 模块路径,后跟 @ 符号,后跟分支名称:

go get your_domain/sammy/awesome@new-feature


运行此命令将导致 go 连接到 your_domain/sammy/awesome 存储库,在分支的当前最新提交处下载新功能分支,并将该信息添加到 go.mod 文件中。

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

以您的模块的 Cobra 依赖项为例,假设您需要引用 github.com/spf13/cobra 的 commit 07445ea,因为它有一些您需要的更改,并且由于某种原因您不能使用其他版本。在这种情况下,您可以在 @ 符号之后提供提交哈希,就像为分支或标签提供的一样。使用模块和版本在 mymodule 目录中运行 go get 命令以下载新版本:

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


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

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 命令中使用 @ 符号,就像使用非版本标记或分支一样。现在,通过运行 go get 命令以 v1.1.1 作为版本来更新您的模块以使用 Cobra 1.1.1:

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


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

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,但您确定更愿意开始使用最新版本,则可以使用特殊的最新版本来做到这一点.要将您的模块更新到最新版本的 Cobra,请使用模块路径和最新版本再次运行 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 文件中的依赖项,而无需手动编辑它。正如您在本节中看到的,使用带有模块名称的 @ 字符允许您使用模块的特定版本,从发布版本到特定的存储库提交。它甚至可以用于返回到最新版本的依赖项。使用这些选项的组合将允许您确保将来程序的稳定性。

结论


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

有关 Go 模块的更多信息,Go 项目有一系列关于 Go 工具如何与模块交互和理解模块的博客文章。 Go 项目在 Go Modules Reference 中也有关于 Go 模块的非常详细和技术性的参考。

文章链接