如何使用 Go 模块

作者选择了 多样性在技术基金作为 写给捐款计划的一部分接受捐款。

介绍

在版本1.13中,Go的作者添加了一种新的方式来管理Go项目所依赖的图书馆,称为Go模块(https://golang.org/ref/mod)。Go模块被添加以响应日益增长的需求,以便开发人员更容易地维护其依赖的各种版本,并在开发人员在计算机上组织项目的方式上增加了更多的灵活性。Go模块通常由一个项目或库组成,并包含一系列Go包,然后一起发布。Go模块解决了原始系统的许多问题,允许用户将他们的代码项目放入他们选择的目录,并为每个模块指定依赖的版本。

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

前提条件

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

  • Go 版本 1.16 或更高版本已安装. 要设置此功能,请遵循操作系统的 How To Install Go 教程。
  • 熟悉在 Go 中写包。

创建新模块

乍一看,一个Go模块看起来很像一个Go包(https://andsky.com/tech/tutorials/how-to-write-packages-in-go)。一个模块有许多Go代码文件来实现一个包的功能,但它也有两个额外的和重要的文件在根:go.mod文件和go.sum文件。

首先要做的是决定该模块将生活在哪个目录. 随着Go模块的引入,它成为可能的Go项目在文件系统的任何地方,而不仅仅是一个特定的目录由Go定义. 你可能已经有一个目录为你的项目,但在本教程中,你将创建一个名为项目的目录,新模块将被称为mymodule

如果您正在使用命令行,请开始创建项目目录并导航到它:

1mkdir projects
2cd projects

接下来,您将创建模块目录本身. 通常,模块的顶级目录名称与模块名称相同,这使得事情更容易跟踪。

1mkdir mymodule

一旦您创建了模块目录,目录结构将如下:

1└── projects
2    └── mymodule

下一步是在mymodule目录中创建一个go.mod文件来定义Go模块本身。 要做到这一点,你将使用go工具的mod init命令,并给它提供模块的名称,在这种情况下是mymodule

1go mod init mymodule

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

1[secondary_label Output]
2go: creating new go.mod: module mymodule

有了创建的模块,您的目录结构现在将看起来像这样:

1└── projects
2    └── mymodule
3        └── go.mod

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

了解go.mod文件

当您使用go工具运行命令时,go.mod文件是过程的一个非常重要的部分. 它是包含模块名称和其他模块版本的文件,您自己的模块取决于它。

mymodule目录中,使用nano或您最喜欢的文本编辑器打开go.mod文件:

1nano go.mod

内容将看起来类似于此,这并不多:

1[label projects/mymodule/go.mod]
2module mymodule
3
4go 1.16

第一行,‘模块’指令,告诉Go你的模块的名称,所以当它在一个包中查看‘导入’路径时,它不知道在其他地方寻找‘mymodule’。

1module mymodule

在此时,文件中唯一的其他行,即go指令,告诉Go该模块的目标语言的版本。

1go 1.16

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

您现在已经创建了一个 Go 模块,并查看了初始的 go.mod 文件所包含的内容,但您的模块还没有做任何事情。

将Go代码添加到您的模块中

为了确保模块被正确创建,并添加代码,这样你就可以运行你的第一个Go模块,你会创建一个main.go文件在mymodule目录中。在Go程序中,main.go文件通常被用来信号一个程序的起点。文件的名称并不像内部的主要函数那么重要,但匹配两者使其更容易找到。

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

1nano main.go

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

1[label projects/mymodule/main.go]
2package main
3
4import "fmt"
5
6func main() {
7    fmt.Println("Hello, Modules!")
8}

在Go中,每个目录都被认为是自己的包,每个文件都有自己的声明行.在你刚刚创建的main.go文件中,被命名为main。通常,你可以以你想要的方式命名包,但在Go中main包是特殊的。

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

最后,定义了主要函数。主要函数是Go中与主要包相关的另一个特殊案例。当Go在一个名为主要的包内看到一个名为主要的函数时,它知道主要函数是它应该运行的第一个函数。

一旦您创建了 main.go 文件,模块的目录结构将看起来像这样:

1└── projects
2    └── mymodule
3        └── go.mod
4        └── main.go

如果您熟悉使用 Go 和 ‘GOPATH’,则模块中的运行代码与您从 GOPATH 的目录中执行的操作相似(如果您不熟悉 GOPATH,请不要担心,因为使用模块会取代其使用)。

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

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

1go run main.go

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

1[secondary_label Output]
2Hello, Modules!

在本节中,您已将一个main.go文件添加到您的模块中,其初始的main函数打印了Hello, Modules! 在此时,您的程序尚未从Go模块中受益,它可以在您的计算机上任何地方运行go run的文件。Go模块的第一个真正的好处是能够在任何目录中添加到您的项目的依赖性,而不仅仅是GOPATH目录结构。

向您的模块添加一个包

类似于标准的Go包,一个模块可以包含任何数量的包和子包,或者它可能根本不包含任何包. 对于这个示例,您将在mymodule目录中创建一个名为mypackage的包。

使用mypackage参数在mymodule目录中运行mkdir命令来创建此新包:

1mkdir mypackage

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

1└── projects
2    └── mymodule
3        └── mypackage
4        └── main.go
5        └── go.mod

使用cd命令将目录更改为新的mypackage目录,然后使用nano或您最喜欢的文本编辑器创建一个mypackage.go文件。

1cd mypackage
2nano mypackage.go

mypackage.go文件中,添加一个名为PrintHello的函数,在呼叫时将打印Hello, Modules! This is mypackage speaking!消息:

1[label projects/mymodule/mypackage/mypackage.go]
2package mypackage
3
4import "fmt"
5
6func PrintHello() {
7    fmt.Println("Hello, Modules! This is mypackage speaking!")
8}

由于您希望PrintHello函数从另一个软件包中可用,所以函数名中的P字母很重要。 大字母意味着该功能被导出并可用于任何外部程序。

现在你已经创建了具有导出函数的mypackage包,你需要从mymodule包中导入它来使用它,这类似于你以前将导入其他包,如fmt包,但这一次你将包含你的模块名称在导入路径的开始。

 1[label projects/mymodule/main.go]
 2
 3package main
 4
 5import (
 6    "fmt"
 7
 8    "mymodule/mypackage"
 9)
10
11func main() {
12    fmt.Println("Hello, Modules!")
13
14    mypackage.PrintHello()
15}

如果你更仔细地看一下导入陈述,你会看到新的导入开始于mymodule,这是你在go.mod文件中设置的模块名称。

1"mymodule/mypackage"

未来,如果您在mypackage中添加包裹,您也会以类似的方式将其添加到导入路径的尽头,例如,如果您在mypackage中有另一个名为extrapackage的包裹,则该包裹的导入路径将是mymodule/mypackage/extrapackage

mymodule目录中运行go runmain.go的更新模块,如前所述:

1go run main.go

当您重新运行模块时,您将看到来自以前的Hello, Modules!消息,以及来自新mypackagePrintHello函数的新消息:

1[secondary_label Output]
2Hello, Modules!
3Hello, Modules! This is mypackage speaking!

您现在已经将新包添加到您的初始模块中,创建了一个名为mypackage的目录,具有PrintHello函数. 随着模块的功能的扩展,您可以开始使用其他人的模块。

将远程模块添加为依赖

Go 模块是从版本控制存储库分发的,通常是 Git 存储库。当你想将一个新模块添加到自己的存储库中时,你可以使用存储库的路径来引用你想要使用的模块。

对于这个例子,你将添加到你的模块中的 github.com/spf13/cobra库的依赖性。

类似于你创建mymodule模块时,你会再次使用go工具,但是,这一次,你会从mymodule目录中运行go get命令。运行go get并提供你想要添加的模块。

1go get github.com/spf13/cobra

当您运行此命令时,Go工具将从您指定的路径搜索Cobra存储库,并通过查看存储库的分支和标签来确定Cobra的哪个版本是最新的。

现在,在mymodule目录中打开go.mod文件,看看go工具在添加新依赖时如何更新go.mod文件。下面的示例可能取决于发布的Cobra的当前版本或您正在使用的Go工具的版本,但更改的整体结构应该相似:

 1[label projects/mymodule/go.mod]
 2module mymodule
 3
 4go 1.16
 5
 6require (
 7    github.com/inconshreveable/mousetrap v1.0.0 // indirect
 8    github.com/spf13/cobra v1.2.1 // indirect
 9    github.com/spf13/pflag v1.0.5 // indirect
10)

使用要求指令的新部分已被添加,该指令告诉Go你想要的模块,如github.com/spf13/cobra和你添加的模块的版本,有时要求指令还会包含一个//间接评论,这个评论说,在要求指令被添加时,模块在模块的任何源文件中都没有直接参考。

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

一旦你下载了依赖性,你就会想用一些最小的Cobra代码更新你的main.go文件以使用新的依赖性,更新你的main.go文件以下面的Cobra代码在mymodule目录中使用新的依赖性:

 1[label projects/mymodule/main.go]
 2package main
 3
 4import (
 5    "fmt"
 6
 7    "github.com/spf13/cobra"
 8
 9    "mymodule/mypackage"
10)
11
12func main() {
13    cmd := &cobra.Command{
14    	Run: func(cmd *cobra.Command, args []string) {
15    		fmt.Println("Hello, Modules!")
16
17    		mypackage.PrintHello()
18    	},
19    }
20
21    fmt.Println("Calling cmd.Execute()!")
22    cmd.Execute()
23}

此代码创建了一个cobra.Command结构,具有包含现有的Hello陈述的Run函数,然后通过呼叫到cmd.Execute()来执行。

1go run main.go

您将看到以下输出,它看起来与之前所看到的类似,但这一次,它使用你的新依赖性,如Calling cmd.Execute()! 行所示:

1[secondary_label Output]
2Calling cmd.Execute()!
3Hello, Modules!
4Hello, Modules! This is mypackage speaking!

使用去获取来添加远程依赖的最新版本,例如github.com/sp13/cobra在这里,使您更容易以最新的错误修复来保持依赖的更新。

使用一个模块的特定版本

由于Go模块来自版本控制存储库,它们可以使用版本控制功能,如标签,分支,甚至承诺。 您可以使用模块路径末尾的 @ 符号来引用这些在您的依赖中,以及您想要使用的版本。 早些时候,当您安装 Cobra 最新版本时,您正在利用此功能,但您不需要将其明确添加到您的命令中。 go 工具知道如果没有使用 @ 提供特定版本,它应该使用特殊版本 latest

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

1go get github.com/spf13/cobra@latest

现在,想象一下,你正在使用的模块正在开发中。 对于这个例子,请将其称为your_domain/sammy/awesome。 这个奇妙模块中添加了一项新功能,并在一个名为新功能的分支中进行工作。

1go get your_domain/sammy/awesome@new-feature

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

然而,分支不是您可以使用 @ 选项的唯一方法. 此语法可以用于标签,甚至可以用于特定承诺到存储库. 例如,有时您正在使用的库的最新版本可能有一个破碎的承诺。

以您的模块的Cobra依赖为例,假设您需要引用commit 07445eagithub.com/spf13/cobra,因为它有一些你需要的更改,你不能使用其他版本的某种原因。

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

如果您再次打开您的模块的go.mod文件,您将看到go get更新了要求字段的github.com/spf13/cobra,以参考您指定的 commit:

 1[label projects/mymodule/go.mod]
 2module mymodule
 3
 4go 1.16
 5
 6require (
 7    github.com/inconshreveable/mousetrap v1.0.0 // indirect
 8    github.com/spf13/cobra v1.1.2-0.20210209210842-07445ea179fc // indirect
 9    github.com/spf13/pflag v1.0.5 // indirect
10)

由于 commit 是一个特定的时间点,而不是一个标签或分支,Go 将要求指令中包含额外的信息,以确保它在未来使用正确的版本。

Go 模块还使用此功能来支持该模块的不同版本的发布。当 Go 模块发布新版本时,将新标签添加到存储库中,以版本号为标签。

回到 Cobra 作为一个例子,假设你想使用 Cobra 版本 1.1.1。你可以看看 Cobra 存储库,并看到它有名为 v1.1.1 的标签,例如. 要使用这个标记版本,你会在一个 go get 命令中使用 @ 符号,就像你会使用一个非版本的标签或分支一样。

1go get github.com/spf13/[email protected]

现在,如果你打开你的模块的go.mod文件,你会看到go get更新了要求字段为github.com/spf13/cobra,以参考你提供的版本:

 1[label projects/mymodule/go.mod]
 2module mymodule
 3
 4go 1.16
 5
 6require (
 7    github.com/inconshreveable/mousetrap v1.0.0 // indirect
 8    github.com/spf13/cobra v1.1.1 // indirect
 9    github.com/spf13/pflag v1.0.5 // indirect
10)

最后,如果您正在使用某个特定版本的库,例如07445ea commit 或v1.1.1,但您决定您宁愿使用最新版本,则可以使用特殊的最新版本来完成此操作。

1go get github.com/spf13/cobra@latest

一旦这个命令完成,‘go.mod’文件将更新,看起来像在你引用特定版本的Cobra之前一样。 根据你的版本的Go和当前的最新版本的Cobra,你的输出可能看起来有点不同,但你仍然应该看到在‘要求’部分的‘github.com/spf13/cobra’行重新更新到最新版本:

1module mymodule
2
3go 1.16
4
5require (
6    github.com/inconshreveable/mousetrap v1.0.0 // indirect
7    github.com/spf13/cobra v1.2.1 // indirect
8    github.com/spf13/pflag v1.0.5 // indirect
9)

go get命令是一个强大的工具,你可以用来管理你的go.mod文件中的依赖,而无需手动编辑它.正如你在本节中所看到的,使用一个模块名称的@字符允许你使用模块的特定版本,从发布版本到特定存储承诺。

结论

在本教程中,您创建了一个 Go 模块与子包,并在您的模块中使用该包. 您还将另一个模块添加到您的模块中作为依赖,并探索如何以各种方式参考模块版本。

有关 Go 模块的更多信息,Go 项目有 一系列博客帖子关于 Go 工具如何与模块互动和理解。

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

Published At
Categories with 技术
Tagged with
comments powered by Disqus