【Golang实战】开始使用 GO MODULE 包管理工具

这篇文章是基于 Using Go Modules 翻译或者摘抄过来的。

在 go module 包管理工具发布之后,我就一直关注着它的使用情况。并且曾在第一时间观看过相关的 youtobe 视频。但是由于对 go module 包管理的概念还比较模糊,一直没掌握住要领,导致项目 go module 迁移一直搁置。

我的 golang 项目没有加入包管理的概念,一直以来都是采用 GOPATH 管理相关的依赖,因此我的所有 golang 项目都在 $GOPATH/src 目录下面,项目很集中,目录结构也没那么好看,因为最外层嵌套了 src 目录,我不是很喜欢。

垂涎 go module 这个功能很久了,一直没机会一睹芳容。也许是缘分,昨天偶然间看到了 golang.org 介绍 go module 使用的博客,于是就跟着博客学习了。

Using Go Modules 这篇文章对于 go module 的使用讲的很详细。但我依旧反复看了不下3遍,才慢慢的掌握要领。

第一遍是粗略的浏览博文,了解 go module 的概念,以及它的大致使用。读第二遍是因为跟着博客教程实践,在迁移项目的时候遇到了问题,不得其解,所以又细细的读了第二遍,关注到了一些细节,让项目能够顺利的迁移。而第三遍阅读,则是为了查缺补漏,找到一些还没有发现到的重要细节。而今天,也是第四遍阅读,是为了摘录我在学习中发现的一些细节,完成这篇博文,为后续学习的同学提供小抄。

话不多说了,下面跟博文一起学习使用 go module 吧。

A module is a collection of Go packages stored in a file tree with a go.mod file at its root. The go.mod file defines the module’s module path, which is also the import path used for the root directory, and its dependency requirements, which are the other modules needed for a successful build. Each dependency requirement is written as a module path and a specific semantic version.

原文中介绍到,go module 是一个包管理工具,将项目的依赖包存储在了项目根目录下的 go.mod 文件中。 go.mod 文件定义了该模块的路径、项目的导入路径、以及该模块的依赖等等。

As of Go 1.11, the go command enables the use of modules when the current directory or any parent directory has a go.mod, provided the directory is outside $GOPATH/src. (Inside $GOPATH/src, for compatibility, the go command still runs in the old GOPATH mode, even if a go.mod is found. See the go command documentation for details.) Starting in Go 1.13, module mode will be the default for all development.

Go 1.11 之后的版本中,go 命令行允许 $GOPATH/src 之外的包含 go.mod 文件的目录使用 module 功能,但是 $GOPATH/src 中的项目,仍保持原有的包管理方式。并且还提到,在 Go 1.13 之后的版本将会默认使用 go module 包管理功能。

原文中从6个方面介绍了如何使用 Go module,也给出了具体的示例。但是博主依照示例练习时,并不能正确访问示例地址,因此在演示示例的同时,博主会创建一个 Github 示例项目,为根据这篇文章学习的同学提供便利。

  • Creating a new module.
  • Adding a dependency.
  • Upgrading dependencies.
  • Adding a dependency on a new major version.
  • Upgrading a dependency to a new major version.
  • Removing unused dependencies.

Creating a new module

在介绍的时候也讲到,Go 1.13 版本之后,才会默认使用 go module 包管理。所以在 Go 1.13 版本之前,包管理还是使用的 GOPATH,因此创建一个新 module 时,应该在 $GOPATH/src 目录之外。

那么首先创建一个目录 moduletest 并进入目录创建一个 hello.go 文件,内容如下

package moduletest

func Hello() string {
	return "Hello World"
}

再创建一个 hello_test.go 测试文件,内容如下

package moduletest

import "testing"

func TestHello(t *testing.T) {
	want := "Hello World"
	if got := Hello(); got != want {
		t.Errorf("Hello() = %q, want %q", got, want)
	}
}

如果执行 go test 命令,则可以看到如下的输出

$ go test
PASS
ok      _/C_/Users/dev/go-project/example/moduletest    0.325s

原文描述如下,即因为 go command 知道没有导入路径,所以基于当前执行路径补全了一个伪造的路径。

The last line summarizes the overall package test. Because we are working outside $GOPATH and also outside any module, the go command knows no import path for the current directory and makes up a fake one based on the directory name。

接下来使用 go mod 命令初始化这个项目,让其生成 go.mod 文件,并指定其模块名为项目所在的github路径 github.com/fyf2173/moduletest,然后再执行 go test 命令。

$ go mod init github.com/fyf2173/moduletest
go: creating new go.mod: module github.com/fyf2173/moduletest

$ go test
PASS
ok      github.com/fyf2173/moduletest   0.330s

命令执行成功之后,会在项目根目录下生成一个 go.mod 文件。原文中介绍到,如果 moduletest 存在子目录,在子目录中不需要执行 go mod init 命令,因为子目录会被当成 github.com/fyf2173/moduletest 模块的一部分,而且此时子目录的导入路径已经变成了 github.com/fyf2173/moduletest + subdirectory,例如,如果有一个名为 world 的子目录,则它的导入路径为 github.com/fyf2173/moduletest/world

$ cat go.mod
module github.com/fyf2173/moduletest

go 1.12

Adding a dependency

为了测试添加依赖,我创建了一个公共测试库 github.com/fyf2173/moduledependencytest ,它只包含了一个 hello.go 文件,内容如下

package moduledependencytest

func Hello() string {
	return "Hello World"
}

公共测试库创建好了之后,在测试项目中导入这个公共库,更新后的内容如下:

package moduletest

import "github.com/fyf2173/moduledependencytest"

func Hello() string {
	return moduledependencytest.Hello()
}

github.com/fyf2173/moduledependencytest 虽然导入了,但是没有添加到 module 中管理,仍然是不可用的,此时可以执行 go test 命令,它会检测依赖包,并更新 go.mod 文件。从以下输出内容中可以看到,执行 go test 命令后,go command 会去寻找依赖库并将它下载到本地,同时也更新 go.mod 文件。FAIL 是因为在代码中导入了,但是未使用测试库。

$ go test
go: finding github.com/fyf2173/moduledependencytest latest
go: downloading github.com/fyf2173/moduledependencytest v0.0.0-20200401090115-a3e2d76f2f1a
go: extracting github.com/fyf2173/moduledependencytest v0.0.0-20200401090115-a3e2d76f2f1a
# github.com/fyf2173/moduletest [github.com/fyf2173/moduletest.test]
.\hello.go:3:8: imported and not used: "github.com/fyf2173/moduledependencytest"
FAIL    github.com/fyf2173/moduletest [build failed]

$ cat go.mod
module github.com/fyf2173/moduletest

go 1.12

require github.com/fyf2173/moduledependencytest v0.0.0-20200401090115-a3e2d76f2f1a

The go command resolves imports by using the specific dependency module versions listed in go.mod. When it encounters an import of a package not provided by any module in go.mod, the go command automatically looks up the module containing that package and adds it to go.mod, using the latest version. (“Latest” is defined as the latest tagged stable (non-prerelease) version, or else the latest tagged prerelease version, or else the latest untagged version.)

原文中提到,go command 会按照 go.mod 文件中列出的使用了特定版本号的依赖模块处理导入包。当 go.mod 文件中没有相关依赖时,它会去查询最新版本的,使用了 module 管理的依赖包。在示例中,因为依赖库没有tag,所以 go command 将 master 分支的最新代码加入到了 go.mod 文件,并将它的版本号设定为 v0.0.0,因为它是一个 untagged commit。

go list -m all 命令可以查看当前模块的所有依赖项,如下

$ go list -m all
github.com/fyf2173/moduletest
github.com/fyf2173/moduledependencytest v0.0.0-20200401090115-a3e2d76f2f1a

Upgrading dependencies

With Go modules, versions are referenced with semantic version tags. A semantic version has three parts: major, minor, and patch. For example, for v0.1.2, the major version is 0, the minor version is 1, and the patch version is 2. Let’s walk through a couple minor version upgrades. In the next section, we’ll consider a major version upgrade.

原文中提到,在 Go modules 中,版本号控制采用的是 semantic version tags,它包含三部分:主版本号,次版本号,以及补丁版本号,如v0.1.2,主版本号是0,次版本号是1,补丁版本号是2。

在示例代码中,公共测试库还没有新增任何tag,所以项目依赖中采用的 untagged 代码,即 master 分支代码。为了测试依赖更新,我在 master 分支代码基础上新增了 v1.0.0 版本号。然后使用 go list -m -versions github.com/fyf2173/moduledependencytest 查看公共库的可用版本号列表。输出如下:

$ go list -m -versions github.com/fyf2173/moduledependencytest
github.com/fyf2173/moduledependencytest v1.0.0

现在执行 go get github.com/fyf2173/moduledependencytest 更新到最新版本。输出如下

$ go get github.com/fyf2173/moduledependencytest
go: finding github.com/fyf2173/moduledependencytest v1.0.0
go: downloading github.com/fyf2173/moduledependencytest v1.0.0
go: extracting github.com/fyf2173/moduledependencytest v1.0.0

$ cat go.mod
module github.com/fyf2173/moduletest

go 1.12

require github.com/fyf2173/moduledependencytest v1.0.0

$ go list -m all
github.com/fyf2173/moduletest
github.com/fyf2173/moduledependencytest v1.0.0

可以看到,go command 拉取了 v1.0.0 版本的公共库代码,并更新了 go.mod 文件中的版本号。

如果当前主版本下有新的不同的修订版本代码,但是最新修订版本代码又有着破坏性更新,只希望更新到某个更小的修订版,这时候就需要指定版本号更新,例:go get github.com/fyf2173/[email protected]

Adding a dependency on a new major version

Each different major version (v1, v2, and so on) of a Go module uses a different module path: starting at v2, the path must end in the major version. In the example, v3 of rsc.io/quote is no longer rsc.io/quote: instead, it is identified by the module path rsc.io/quote/v3. This convention is called semantic import versioning, and it gives incompatible packages (those with different major versions) different names. In contrast, v1.6.0 of rsc.io/quote should be backwards-compatible with v1.5.2, so it reuses the name rsc.io/quote. (In the previous section, rsc.io/sampler v1.99.99 should have been backwards-compatible with rsc.io/sampler v1.3.0, but bugs or incorrect client assumptions about module behavior can both happen.)

原文中提到,不同主版本号的 go module 会使用不同的路径,在引入新的 module 包时,如果主版本号大于1,则引入路径必须以主版本号结尾,例:github.com/fyf2173/moduledependencytest/v2,即指引入了公共测试库v2版本。而且文中还提到,每个主版本号都应该是向后兼容的,因为一个主版本号的库只能导入一次。

跟原文中一样,我们在公共测试库中新增一个 Proverb 函数,并将版本号更新到 v2,然后导入该版本号的公共库(github.com/fyf2173/moduledependencytest/v2),并在我们的测试模块中使用它。

公共库中 hello.go 的代码更新如下

package moduledependencytest

func Hello() string {
	return "Hello World"
}

func Proverb() string {
	return "proverb"
}

在测试模块中导入 v2 版本的公共库,并调用该版本中新增的 Proverv 函数,内容如下:

package moduletest

import (
	"github.com/fyf2173/moduledependencytest"
	moduledependencytestV2 "github.com/fyf2173/moduledependencytest/v2"
	)

func Hello() string {
	return moduledependencytest.Hello()
}

func Proverb() string {
	return moduledependencytestV2.Proverb()
}

然后在 hello_test.go 中新增一个测试案例:

func TestProverb(t *testing.T) {
	want := "proverb"
	if got := Proverb(); got != want {
		t.Errorf("Proverb() = %q, want %q", got, want)
	}
}

再然后执行 go test 命令,让代码自动导入 v2 版本的公共库

$ go test
go: finding github.com/fyf2173/moduledependencytest/v2 v2.0.0
go: downloading github.com/fyf2173/moduledependencytest/v2 v2.0.0
hello.go:5:2: unknown import path "github.com/fyf2173/moduledependencytest/v2": cannot find module providing package github.com/fyf2173/moduledependencytest/v2

$ go get github.com/fyf2173/[email protected]
go: finding github.com/fyf2173/moduledependencytest v2.0.0
go: downloading github.com/fyf2173/moduledependencytest v2.0.0+incompatible
go: extracting github.com/fyf2173/moduledependencytest v2.0.0+incompatible

$ cat go.mod
module github.com/fyf2173/moduletest

go 1.12

require github.com/fyf2173/moduledependencytest v2.0.0+incompatible

从以上输出结果可以看出,我在导入 github.com/fyf2173/moduledependencytest/v2 公共库时测试是不通过的,提示找不到 module 库。为什么呢?原文中指出,也是我们容易忽略的,只有在 go module 库中才允许带版本号的导入方式。所以我们还需要将 github.com/fyf2173/moduledependencytest 的包管理方式迁移为 go module 包管理。Migrating to Go modules in your project 描述了如何将已有项目迁移至 go module .

由于某些原因,一直未通过测试,暂且放一放。

Upgrading a dependency to a new major version

Removing unused dependencies

使用 go mod tidy 命令可以清理未使用的依赖。

$ cat go.sum
github.com/fyf2173/moduledependencytest v0.0.0-20200402073601-1cda0fcad446 h1:aoJoqj/Ndd3tH0B+VChf5vYZasFahOEQCz4bDbGbJ9Q=
github.com/fyf2173/moduledependencytest v0.0.0-20200402073601-1cda0fcad446/go.mod h1:RrbOG2VU+Ogf5XXYW/hCmApZs4EVYBzOTH2C1YnPRak=
github.com/fyf2173/moduledependencytest v2.0.0+incompatible/go.mod h1:I7a6S4ZNByCtSIkOXHyHDSjFjBbiSD7eH7d9uRtONFg=

$ go mod tidy

$ cat go.sum
github.com/fyf2173/moduledependencytest v2.0.0+incompatible/go.mod h1:I7a6S4ZNByCtSIkOXHyHDSjFjBbiSD7eH7d9uRtONFg=

Conclusion

  • go mod init creates a new module, initializing the go.mod file that describes it.
  • go build, go test, and other package-building commands add new dependencies to go.mod as needed.
  • go list -m all prints the current module’s dependencies.
  • go get changes the required version of a dependency (or adds a new dependency).
  • go mod tidy removes unused dependencies.

Migrating to Go modules in your project

原文中介绍了一个现有项目在迁移至 go module 之前必然存在的三种状态

  • A brand new Go project
  • An established Go project with a non-modules dependency manager
  • An established Go project without any dependency manager

第一种状态的项目在 Creating a new module 部分就已经介绍过,而从未使用过任何依赖管理工具,或者使用了非 go module 管理工具的旧项目迁移至 go module 则是这部分内容重点介绍的,就以 github.com/fyf2173/moduledependencytest 公共测试库为例。在前面已经知道,该库版本已经更新到了 v2 版本,在执行单元测试的时候就已经告知我们,它是一个不兼容的库。现在就开始将它迁移至 go module 吧。

注意到该公共库没有使用过任何的依赖管理工具,因此我们可以使用 go mod init 命令直接将其初始化。

$ go mod init github.com/fyf/moduledependencytest
go: creating new go.mod: module github.com/fyf/moduledependencytest

$ cat go.mod
module github.com/fyf/moduledependencytest

go 1.12

因为这个公共测试库很简单,也没有任何的依赖,所以可以直接使用 go mod init github.com/fyf/moduledependencytest 对其初始化。设置 module path 为 github.com/fyf/moduledependencytest 是因为它在 github 中的路径就是这个,也没有导入它自身的任何子模块,而且其他使用了该库的导入路径也是它。

module path 是可以自定义的,如果现有项目比较大,依赖也比较多,而且子模块间有相互导入,那么 module path 定义为项目名即可。weread 是一个没有使用任何依赖管理工具的项目,项目依赖比较多,子模块间也有相互调用,而且是在 $GOPATH/src 目录下,子模块间互相调用时的导入路径都是以 weread 开头,为了尽量少的改动代码,应该将该项目的 module path 设置为 weread,即使用 go mod init weread 命令初始化项目,并使用 go mod tidy 添加项目的其他依赖。

你可能感兴趣的:(golang)