如何使用Go模块

3,697 阅读17分钟

简介

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

在本教程中,你将创建你自己的公共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

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

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

其内容将与此相似,这并不重要。

projects/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 函数中打印出Hello, Modules! 消息。

projects/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 函数来将"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! 文本。

OutputHello, 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 包中import 来使用它。这与你导入其他包的方法类似,比如之前的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 runmain.go 目录下运行你的更新模块。

go run main.go

当你再次运行该模块时,你会看到先前的Hello, Modules! 信息和新的mypackage'sPrintHello 函数打印的新信息。

OutputHello, 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 指令也会包括一个// indirect 的注释。这个注释表示,在添加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()! 行所示。

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

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

使用模块的特定版本

由于Go模块是从版本控制库中发布的,它们可以使用版本控制功能,如标签、分支,甚至提交。你可以在你的依赖关系中使用@ 符号在模块路径的末尾加上你想使用的版本来引用这些。早些时候,当你安装最新版本的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 依赖关系为例,假设你需要引用提交的 07445ea github.com/spf13/cobra 因为它有一些你需要的修改,而你又因为某些原因不能使用其他版本。在这种情况下,你可以在@ 符号后面提供提交的哈希值,就像你对分支或标签一样。在你的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 命令来更新你的模块,以使用 Cobra 1.1.1。 v1.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 commit或者之前的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模块提供了非常详细的技术参考。

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