详解Go 中的包和模块 (Golang) - 第 2 部分

165 阅读5分钟

现在让我们看看当前的教程。以下是当前教程的目录。

概述

在上一篇教程中,我们详细了解了包和模块概述。

在本教程中,我们将重点关注模块

模块(Module)类型

我们了解到 module 是一个包含嵌套 go 包的目录。因此本质上模块可以被视为仅包含嵌套包的包。我们在包教程中看到包可以是可执行包或实用程序包(不可执行)。与包类似,模块可以有两种类型。

  • 可执行模块——我们已经知道main是GoLang中的可执行包。因此,包含主包的模块是可执行模块。main将包含一个表示程序启动的main函数。安装具有主包的模块时,它将在 $GOBIN 目录中创建一个可执行文件。

  • 不可执行模块或实用程序模块– 除主包之外的任何包都是不可执行包。它不是自我可执行的。它只包含实用程序函数和可由可执行包使用的其他实用程序。因此,如果模块不包含包,那么它将是不可执行的或实用程序模块。该模块旨在用作实用程序,并将由其他模块导入。

为模块创建可执行文件(仅适用于带有主包的模块)

  • 执行 go build ,它将在当前目录中创建可执行文件

  • 执行 go install ,它将在 $GOBIN 目录中创建可执行文件

Packages与Module

根据模块定义,它是一个目录,其根目录包含嵌套且相关的 go 包go.mod的集合。go.mod文件定义了

  • 模块导入路径。
  • 成功构建的模块的依赖关系要求。它定义了两个项目的依赖项要求,并将它们锁定到正确的版本

模块可以提供

  • 依赖管理
  • 使用模块,go 项目不一定必须位于**$GOPATH/sr** c 文件夹中。

此外,除了go.mod文件之外,go 还保留一个go.sum文件,其中包含所有项目依赖模块的位的加密哈希。这是为了验证您的项目的依赖模块没有更改。

模块内包的行为与之前相同。因此,适用于套餐的内容现在也适用。这一点没有改变。然而,当需要单独对它们进行版本控制时,可以将包的集合称为模块。此外,当它是通用代码并且您希望在多个项目之间共享该代码时。

向您的项目添加依赖项

让我们探索一些向项目添加依赖项的方法

  • 直接添加到go.mod文件中

  • 尝试一下

  • 将依赖项添加到源代码中并执行go mod tidy

在查看每种方法之前,我们再次先创建一个模块

go mod init sample.com/learn

直接添加到go.mod文件中

我们也可以向 go.mod 文件添加直接依赖项。让我们这样做吧

将以下依赖项添加到go.mod文件

require github.com/pborman/uuid v1.2.1

有了这个依赖项,go.mod 文件将如下所示

module sample.com/learn

go 1.14

require github.com/pborman/uuid v1.2.1

现在我们还需要下载新添加的依赖项。我们可以使用以下命令

go mod download

此命令将下载github.com/pborman/uuid模块及其所有依赖项。此外,它还会使用所有直接和间接依赖项的校验和和版本更新go.sum文件。go build 和 go install 也会下载依赖项并构建二进制文件。go run 还将下载并运行二进制文件。当您想要预下载依赖项而不构建或运行它时,请使用 go mod download 命令。

尝试一下

只需执行 go get 也会在 go.mod 文件中添加依赖项。从 go.mod 文件中删除我们上面添加的 uuid 依赖项并清理 go.sum 文件。现在运行以下命令

export GO111MODULE=on
go get github.com/pborman/uuid

现在检查 go.mod 文件的内容。cat go.mod

module sample.com/learn

go 1.14

require github.com/pborman/uuid v1.2.1 //indirect

该依赖项将被标记为//indirect,因为它未在任何源文件中使用。一旦你在源文件中使用它后进行 go 构建, //indirect 将被 go 自动删除。它还会使用所有直接和间接依赖项的校验和和版本更新go.sum文件。

将依赖项添加到源代码中并执行 go mod tidy

基本上 go mod tidy 命令可确保您的 go.mod 文件反映您在项目中实际使用的依赖项。当我们运行 go mod tidy 命令时,它会做两件事

  • 添加在源文件中导入的任何依赖项

  • 删除go.mod文件中提到但未在任何源文件中导入的任何依赖项。

让我们看一个例子。创建一个导入路径为“ sample.com/learn ”的模块

go mod init sample.com/learn

让我们在同一目录中创建一个名为uuid.go的文件,其中包含以下内容

uuid.go

package main

import (
	"fmt"
	"strings"

	"github.com/pborman/uuid"
)

func main() {
	uuidWithHyphen := uuid.NewRandom()
	uuid := strings.Replace(uuidWithHyphen.String(), "-", "", -1)
	fmt.Println(uuid)
}

请注意,我们也在 uuid.go 中导入了依赖项

"github.com/pborman/uuid"

让我们运行以下命令

go mod tidy

此命令将下载源文件中所需的所有依赖项,并使用该依赖项更新go.mod文件。运行此命令后,我们现在再次检查go.mod文件的内容。cat go.mod

module sample.com/learn

go 1.14

require github.com/pborman/uuid v1.2.1

添加供应商目录

如果您想供应您的依赖项,则可以使用以下命令来实现相同的目的

go mod vendor

它将在您的项目目录中创建一个供应商目录。您还可以将供应商目录签入 VCS(版本控制系统)。这变得很有用,因为不需要在运行时下载任何依赖项,因为它已经存在于签入 VCS 的供应商文件夹中

模块导入路径

我们已经看到模块导入路径是用于导入该模块内所有包的前缀路径

可以通过三种情况来决定模块可以使用什么导入路径名。

  • 该模块是一个实用模块,您计划发布您的模块

  • 该模块是一个实用模块,您不打算发布您的模块

  • 该模块是一个可执行模块

该模块是一个实用模块,您计划发布您的模块

如果您计划发布模块,则模块名称应与托管该模块的存储库的 URL 匹配。Go 尝试使用模块的相同导入路径从 VCS 下载依赖项。

该模块是一个实用模块,您不打算发布您的模块

当您只想在本地使用实用程序模块时,就会出现这种情况。在这种情况下,导入路径可以是任何路径。

该模块是一个可执行模块

在这种情况下,模块导入路径也可以是任何内容。即使您计划将模块提交到 VCS,模块导入路径也可以是非 URL,因为它不会被任何其他模块使用

然而,在创建模块时使用有意义的导入路径是一个很好的做法

在同一模块中导入包

可以使用模块的导入路径+包含该包的目录来导入同一模块中的任何包。为了说明这一点,让我们创建一个模块

  • 制作一个学习目录

  • 创建一个导入路径为sample.com/learn的模块

go mod init sample.com/learn
  • 现在创建main.go(有main包和main函数)

  • 还有 math/math.go – 数学包

主程序

package main

import (
	"fmt"
	"sample.com/learn/math"
)

func main() {
	fmt.Println(math.Add(1, 2))
}

math/math.go

package math

func Add(a, b int) int {
    return a + b
}

看看我们如何在 main.go 文件中导入 math 包

"sample.com/learn/math"

这里的导入路径是模块的导入路径,即sample.com/learn + 包含math包的目录。因此,“ sample.com/learn/math”。嵌套目录中的包也可以用同样的方式导入。它的工作方式是,由于前缀是模块导入路径,因此 go 会知道您正在尝试从同一个模块导入。所以它会直接引用它而不是下载它。

从本地不同模块导入包

有时我们想要导入本地存在的模块。让我们了解如何导入这样的模块。但首先,我们必须创建一个可供其他人使用的模块,然后将其导入到另一个模块中。为此,我们创建两个模块

  • Sample.com/数学模块
  • 学校模块

school模块将调用sample.com/math模块的代码

我们首先创建example.com/math模块,该模块将由school模块使用

  • 建立一个数学目录

  • 创建一个导入路径为sample.com/math的模块

go mod init sample.com/math
  • 在math目录中创建一个包含以下内容的文件math.go
package math

func Add(a, b int) int {
	return a + b
}

现在 让我们创建学校模块

  • 现在在与math目录相同的路径中并排 创建一个school目录

  • 创建模块名称school

go mod init school
  • 现在让我们修改go.mod文件以在 school 模块中导入 math 模块。要导入未推送到 VCS 的本地模块,我们将使用替换目录。替换目录将用您指定的路径替换模块路径。
module school

go 1.14

replace sample.com/math => ../math
  • 创建文件 school.go ,它将使用**example.com /math**模块中的 Add 函数
package main

import (
	"fmt"
	"sample.com/math"
)

func main() {
	fmt.Println(math.Add(2, 4))
}

现在开始跑步

go run school.go

它能够调用**sample.com /math** 模块的Add函数并正确给出输出6。

它还会使用example.com /math模块的版本信息更新 go.mod

module school

go 1.14

replace sample.com/math => ../math

require sample.com/math v0.0.0-00010101000000-000000000000

选择库的版本

要了解GO在选择库版本时的做法(在go.mod文件中指定了两个版本),我们必须首先了解语义版本控制

语义版本控制由用点分隔的三个部分组成。以下是版本控制的格式。

v{major_version}.{minor_version}.{patch_version}

在哪里

  • v – 它只是一个版本的指示符

  • Major_version – 它代表库中不兼容的 API 更改。因此,当库中存在不向后兼容的更改时,在这种情况下,major_version 会递增

  • secondary_version – 它表示以向后兼容的方式对库功能进行的更改。因此,当库中存在一些功能更改但这些更改向后兼容时,在这种情况下,次要版本会增加

  • patch_version – 它以向后兼容的方式表示库中的错误修复。因此,当库的现有功能出现错误修复时,在这种情况下 patch_version 就会增加。

现在可能有两种情况

  • 使用同一库的两个版本,仅次要版本和补丁版本不同。他们的主要版本是相同的。

  • 使用同一库的两个版本,但主要不同。

让我们看看在上述两种情况下 go 遵循什么方法

次要版本或补丁版本不同

Go 在选择库版本时遵循最小版本策略方法,其中两个版本在go.mod文件中指定,仅在次要版本或补丁版本上有所不同。

例如,如果您使用同一库的两个版本,它们是

1.2.0

1.3.0

然后 go 将选择 1.3.0,因为它是最新版本。

主要版本不同

Go 将主要版本视为不同的模块本身。现在,这意味着什么?这本质上意味着导入路径将以主版本作为后缀。让我们以任何带有 VCS 的 go 库为例github.com/sample。让我们最新的语义版本是

v8.2.3

然后 go.mod 文件将如下所示

module github.com/sample/v8

go 1.13

..

它的导入路径中有主要版本。因此,任何使用此示例库的库都必须像这样导入它

import "github.com/sample/v8"

如果将来发布v9版本,则必须将其导入到应用程序中,例如

import "github.com/sample/v9"

该库还将更改其 go.mod 文件以反映 v9 主要版本

module github.com/sample/v9

它本质上允许在同一个 go 应用程序中使用同一库的不同主要版本。当同一应用程序中导入同一库的不同主要版本时,我们还可以给出有意义的名称。例如

import sample_v8 "github.com/sample/v8"
import sample_v9 "github.com/sample/v9"

这也称为语义导入版本控制

另请注意

  • 对于第一个版本,可以不在 go.mod 文件中指定版本。

  • 导入同一库的不同主要版本时也要小心。请留意新版本可能提供的新功能。

当您使用更新特定模块时也出于同样的原因

go get -u

那么它只会升级到最新的次要版本或补丁版本(以适用的为准)。例如,假设应用程序使用的当前版本是

v1.1.3

另外假设我们有以下可用版本

v1.2.0
v2.1.0

然后当我们跑步时

go get

然后它会更新为

v1.2.0

原因是因为 go get 只会更新次要版本或补丁版本,但不会更新主要版本,因为 go 将模块的主要版本完全视为不同的模块。

要升级主要版本,请在go.mod文件中显式指定升级的依赖项或执行该版本的 go get 操作。

关于升级模块还有几点需要注意

  • 要将依赖项仅升级到最新的补丁版本,请使用以下命令
go get -u=patch 
  • 要将依赖项升级到特定版本,请使用以下命令
go get dependency@version
  • 要将依赖项升级到特定提交,请使用以下命令
go get @commit_number
  • 要将所有依赖项升级到最新的次要版本和补丁版本,请使用以下命令
go get ./...

去mod命令

以下是 go mod 命令的一些选项。

  • download –它将把所需的依赖项下载到 $GOPATH/pkg/mod/cache 文件夹中。它还会使用所有直接和间接依赖项的校验和和版本更新go.sum文件

  • edit – 用于编辑 go.mod 文件。它提供了一组编辑标志。运行以下命令以查看所有可用编辑标志集

go help mod edit

go help mod edit例如,下面是一些可用的编辑标志

  1. -fmt标志将格式化 go.mod 文件。不会做任何其他改变
  2. -module标志可用于设置模块的导入路径
  • graph – 这可用于打印模块需求依赖关系图

  • init – 我们已经在上面看到了这个命令的用法。它用于初始化一个新模块

  • tidy – 此命令将下载源文件中所需的所有依赖项

  • 供应商– 如果您想供应您的依赖项,则可以使用以下命令来实现相同的目的。它将在您的项目目录中创建一个供应商目录。您还可以将供应商目录签入 VCS(版本控制系统)

  • **verify –**此命令检查当前下载的依赖项的修改。如果任何下载的依赖项已被验证,它将以非零代码退出

  • Why – 该命令分析主模块中的包图表。它打印从主模块到给定包的最短路径。例如,我们在“从本地不同模块导入包”部分中创建的学校模块,如果我们打印如下命令

go mod why sample.com/math

那么下面将是输出

# sample.com/math
school
sample.com/math

输出表明,sample.com /math 包在图中与主模块(此处为 school)相距一定距离。

go.mod 文件中的直接依赖与间接依赖

直接依赖是模块直接导入的依赖。间接依赖是由模块的直接依赖导入的依赖。此外, go.mod文件中提到但未在模块的任何源文件中导入的任何依赖项也被视为间接依赖项。

go.mod文件只记录直接依赖,但以下情况可能会记录间接依赖

  • 直接依赖项的 go.mod 文件中未列出的任何间接依赖项,或者如果直接依赖项没有 go.mod 文件,则该直接依赖项将添加到 go.mod 文件中,并使用 //direct 作为后缀

  • 未在模块的任何源文件中导入的任何依赖项(我们已经在本教程前面看到过这个示例)

go.sum会记录直接依赖和间接依赖的校验和。

go.mod 文件中的间接依赖关系示例

让我们通过一个例子来理解它。为此,我们首先创建一个模块

git mod init sample.com/learn

让我们在 go.mod 文件中添加 colly lib 版本 v1.2.0 作为依赖项。colly 版本 v1.2.0 没有 go.mod 文件。

module sample.com/learn

go 1.14

require github.com/gocolly/colly v1.2.0

现在创建一个文件 learn.go

package main

import (
	"github.com/gocolly/colly"
)

func main() {
	_ = colly.NewCollector()
}

现在进行构建。由于colly v1.2.0版本没有go.mod文件,所以colly所需的所有依赖都会添加到go.mod文件中,并以//indirect为后缀。进行构建。现在检查 go.mod 文件。您将看到以下文件内容

module learn

go 1.14

require (
	github.com/PuerkitoBio/goquery v1.6.0 // indirect
	github.com/antchfx/htmlquery v1.2.3 // indirect
	github.com/antchfx/xmlquery v1.3.3 // indirect
	github.com/gobwas/glob v0.2.3 // indirect
	github.com/gocolly/colly v1.2.0
	github.com/kennygrant/sanitize v1.2.4 // indirect
	github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
	github.com/temoto/robotstxt v1.1.1 // indirect
	golang.org/x/net v0.0.0-20201027133719-8eef5233e2a1 // indirect
	google.golang.org/appengine v1.6.7 // indirect
)

所有其他依赖项均以**//indirect**为后缀。还要检查所有直接和间接依赖项是否都记录在 go.sum 文件中。

结论

这就是 golang 中的包和模块。希望您喜欢这篇文章。请在评论中分享反馈/错误/改进