【Go Module系列教程】手把手教你,如何使用Go Module

【Go Module系列教程】手把手教你如何使用Go Module


请先看我

看了网上的好多中文博客,一直都没有看懂,这个Go Module 到底如何使用。无奈╮(╯▽╰)╭只能去看看官方的教程是怎么样的了。

本文结合 官方文档我自己的实践 ,根据官方文档手动操作了一波Go Module的使用。强烈建议和我一样的小伙伴跟着这篇文章的流程走一遍,肯定有所收货!!!

(文章有点长,慢慢来哈;本人翻译和写作水平有限,如有错误,不吝指正)

个人站点:https://www.quanquanting.com/

简介

这篇博客是【Go Module系列教程】的第一部分,其中包括:

  1. 如何使用Go Module

  2. 如何迁移至Go Module(待更新)

  3. 如何发布Go Module(待更新)

  4. Go Modules: v2 and Beyond(待更新)

Go Module是Golang新的依赖包管理系统,能够直接罗列出依赖包的版本信息且易于管理。在Go 1.11和Go 1.12都已经初步支持Go Module。这篇文章将介绍使用Go Module的基本操作。后续的文章将介绍如何去发布Go Module去给其他人使用。

go.mod文件是在项目的根目录下,是个Go依赖包的集合。这个go.mod文件定义了Go依赖包的路径,也是项目使用的导入路径,还包括使依赖包能够成功构建的依赖需求。每个依赖包都包括一个路径和特定语义版本。

从Go 1.11起,如果目录在$GOPATH/src之外,而且当前的路径或是任何父目录包含go.mod文件时,都可以使用Go Module的命令。相反如果是在$GOPATH/src之中,为了兼容,即使存在go.mod文件,也只能使用原有的GOPATH下的命令。但是从Go 1.13开始,Go Module模式将成为开发的默认模式。

本片文章将介绍开发中一系列常见的操作:

  • 创建一个新Module
  • 添加依赖包
  • 升级依赖包
  • 添加另一个主版本依赖包
  • 将依赖包升级到新的主版本
  • 删除未使用的依赖包

创建一个新Module

让我们创建一个新Module吧

首先需要在$GOPATH/src之外任何地方,创建一个空的目录\hello。进入该目录,在创建一个新的文件hello.go:

package hello 

func Hello() string {
	return "Hello, world."
}

我们再写个测试,hello_test.go:

package hello

import "testing" 

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

到目前为止,这个目录下有个依赖包了,但是还不是个Module,因为还没有go.mod文件。如果我们在目录F:\hello下,执行go test,会看到:

F:\hello>go test
PASS
ok      _/F_/hello 0.153s

最后一行显示了当前的测试结果。因为我们既不在$GOPATH/src之中,也没有Module,所以go不知道当前目录的导入路径,只是根据目录名F:\hello创建一个伪目录。

让我们在该目录下通过go mod init命令,是这个依赖包成为一个Module,然后再执行go test

F:\hello>go mod init hello
go: creating new go.mod: module hello

F:\hello>go test
PASS
ok      hello   0.164s

恭喜你!你写了第一个Module,并通过了测试。

go mod init命令自动创建了go.mod文件:

module hello

go 1.12

go.mod文件只会在Module的根目录。子目录中的依赖包的路径是由Module的目录加上子目录的路径组成。举个例子,如果我们创建了子目录world,我们不需要在这个目录下再运行go mod init,这个依赖包会自动被识别为hello Module的一部分,导入路径就是hello/world

添加依赖包

Go Module的主要目的就提升应用其他开发人员的编写的代码的使用体验(就是添加依赖包的体验)。
现在让我们更新一下hello.go,引入rsc.io/quote,并使用引用函数实现Hello函数:

package hello

import "rsc.io/quote"

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

接下来运行一下go test:

F:\hello>go test
go: finding rsc.io/quote v1.5.2
go: downloading rsc.io/quote v1.5.2
go: extracting rsc.io/quote v1.5.2
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: downloading rsc.io/sampler v1.3.0
go: extracting rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
PASS
ok      hello    0.164s

小知识:以上这一步go test,会自动寻找依赖包,如果你未开代理,应该会出现如下错误:

go: golang.org/x/[email protected]: unrecognized import path "golang.org/x/text" (https fetch: Get https://golang
.org/x/text?go-get=1: dial tcp 216.239.37.1:443: connectex: A connection attempt failed because the connected party did not properly re
spond after a period of time, or established connection failed because connected host has failed to respond.)
go: error loading module requirements

----------------------------------------
https://www.quanquanting.com/
----------------------------------------

如果要继续运行的话,可先配置代理。如果是Windows系统的话,需要在PowerShell中先设置代理:

# 查看
set http_proxy
# 设置
set http_proxy=YOUR-PROXY
set https_proxy=YOUR-PROXY
# 删除
set http_proxy=

例如我的YOUR-PROXYsocks5://127.0.0.1:1080

详细的方法可见==》如何在命令行工具中使用代理?


配置完毕后在运行go test

Go会解析go.mod中所罗列的特定的依赖包并导入。当遇到依赖包被引用,但是不在go.mod之中时,Go会自动查找包含该依赖包的Module并将其添加到go.mod中,默认使用最新的版本(引用顺序为优先寻找“最新标记的稳定版本”,其次寻找“最新发布的预发布版本”,最后时“未标记的版本”)。在我们的例子中,go test命令在导入rsc.io/quote中,导入了rsc.io/quote v1.5.2。同时还下载了两个rsc.io/quote的依赖包,分别是rsc.io/samplergolang.org/x/text。但是只有直接被引用的依赖包被记录到了go.mod文件中:

module hello

go 1.12

require rsc.io/quote v1.5.2

如果再次运行go test不会再重复上面的工作了,因为go mod文件已经在上一次更新了,下载的相应的Module缓存在$GOPATH/pkg/mod中。

F:\hello>go test
PASS
ok      hello   0.174s

这里多说一句,虽然现在Go添加新的依赖包变得更容易了。但是这是有代价的,例如依赖包的质量、安全性和是否授权等,更多详见Russ Cox的文章“Our Software Dependency Problem”

正如我们上面所看到的,在直接添加一个依赖包的过程往往还会带来其他依赖包,go list -m all命令可以列出当前的Module以及它所有的依赖包:

F:\hello>go list -m all
hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0

在输出的列表中,当前的Module总是第一行,让后是按照路径排序的依赖包。

其中golang.org/x/text的版本v0.0.0-20170915032832-14c0d48ead0c是一个伪版本的例子,这是Go针对特定的未作标记提交的版本命名。

除了go.mod文件,Go还创建了名为go.sum的文件,该文件包含特定Module版本内容的Hash:

golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

Go使用go.sum文件确保这些Module和第一次下载的相同,防止你的项目所依赖的Module不会因为恶意、意外或其他原因而被更改。所以go.modgo.sum文件都应该加入到你的版本控制中。

升级依赖包

在Go Module中,版本通过语义版本标签引用,一个语义版本有三个部分:major(主版本号)、minor(次版本号)和patch(修订号)。例如,对v0.1.2来说,其major(主版本号)就是0,其minor(次版本号)就是1,其patch(修订号)就是2。让我们先看看几个次版本的升级,在下一节中来考虑主版本的升级。


小知识:
版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

主版本号:当你做了不兼容的 API 修改,
次版本号:当你做了向下兼容的功能性新增,
修订号:当你做了向下兼容的问题修正。

先行版本号及版本编译元数据可以加到“主版本号.次版本号.修订号”的后面,作为延伸。


go list -m all这个命令的输出,我们可以看到我们使用的是未标记版本的golang.org/x/text。现在把它升级到最新的标记的版本,并测试一下会发现能够正常使用:

F:\hello>go get golang.org/x/text
go: finding golang.org/x/text v0.3.2
go: finding golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
go: downloading golang.org/x/text v0.3.2
go: extracting golang.org/x/text v0.3.2

F:\hello>go test
PASS
ok      hello   0.161s

注意:以上这一步go get golang.org/x/text,同样需要开启代理。

哇哦!一切正常。让我们看看go list -m all

F:\hello>go list -m all
hello
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0

再看看go.mod文件:

module hello

go 1.12

require (
	golang.org/x/text v0.3.2 // indirect
	rsc.io/quote v1.5.2
)

依赖包golang.org/x/text已经升级到了最新的标记的版本(v0.3.2)。go.mod文件也同样的更新了。其中indirect表示这个依赖包不是被我们直接使用,而是其他依赖包用到。可以使用go help modules命令获取更多的信息。

现在我们尝试升级依赖包rsc.io/sampler的次版本。方法同上,先执行go get命令再执行go test:

F:\hello>go get rsc.io/sampler
go: finding rsc.io/sampler v1.99.99
go: downloading rsc.io/sampler v1.99.99
go: extracting rsc.io/sampler v1.99.99

F:\hello>go test
--- FAIL: TestHello (0.00s)
    hello_test.go:8: Hello() = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Hello, world."
FAIL
exit status 1
FAIL    hello   0.179s

哦,不!最新版本的rsc.io/sampler和我们之前用的不兼容了。让我们看看这个依赖包所有可用的标记版本:

F:\hello>go list -m -versions rsc.io/sampler
rsc.io/sampler v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99

我们之前用的是v1.3.0;显然v1.99.99不适用,那我们可以试试v1.3.1:

F:\hello>go get rsc.io/[email protected]
go: finding rsc.io/sampler v1.3.1
go: downloading rsc.io/sampler v1.3.1
go: extracting rsc.io/sampler v1.3.1

F:\hello>go test
PASS
ok      hello   0.181s

注意一下go get中显式参数@v1.3.1。一般来说,每次使用go get命令都可以带上显示的版本;如果不加,默认值是@latest,将解析最新的版本。

添加另一个主版本依赖包

让我们再添加一个函数func Proverb,该函数将调用由依赖包rsc.io/quote/v3提供的quote.Concurrency。首先在hello.go中加入新函数:

package hello

import (
	"rsc.io/quote"
	quoteV3 "rsc.io/quote/v3"
)

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

func Proverb() string {
	return quoteV3.Concurrency()
}

然后在hello_test.go中添加测试函数:

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

最后测试一下:

F:\hello>go test
go: finding rsc.io/quote/v3 v3.1.0
go: downloading rsc.io/quote/v3 v3.1.0
go: extracting rsc.io/quote/v3 v3.1.0
PASS
ok      hello   0.185s

现在可以看到我们的Module同时依赖rsc.io/quotersc.io/quote/v3:

F:\Users\QQT\Documents\Go Projects\NoGOPATH\hello>go list -m rsc.io/q...
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0

每一个不同的主版本(v1、v2、v3等等)对于Go Module都有不同的路径,从第二个主版本开始(v2)路径的结尾必须是主版本号。就像上面这个例子: rsc.io/quotev3不再是rsc.io/quote了,而是rsc.io/quote/v3。这种约定称为**语义导入版本控制**,它为不兼容的依赖包(具有不同版本)提供了不同的名称。相比之下,v1.6.0版本的rsc.io/quote应该向后兼容v1.5.2,因为它们都叫rsc.ip/quote。在前面的小节中,rsc.io/sampler v1.99.99按理来说应该向后兼容rsc.io/sampler v1.3.0,但是bug或错误不可避免。

Go对于一个特定路径的模块,只会构建其中一个版本。意思就是每个主要的版本都只能有一个:例如最多一个rsc.io/quote,一个rsc.io/quote/v2,一个rsc.io/quote/v3等等。这就要求开发者对于单个依赖包的路径有所要求,一个程序构建时不可能同时包含rsc.io/quote v1.5.2rsc.io/quote v1.6.0。但是允许同时存在不同主版本的依赖包,因为它们有着不同的路径。在本例中,我们要使用的quote.Concurrency来自rsc.io/quote/v3,但是还未完全脱离rsc.io/quote v1.5.2的依赖。像这种增量迁移的能力在大型的程序或代码库中显得尤其重要。

将依赖包升级到新的主版本

现在让我们完全从rsc.io/quote转移至rsc.io/quote/v3吧。因为主版本的改变,我们应该料到一些Api可能已经因为不兼容而被删除、重命名或升级。查看一下文档,我们发现Hello函数已经升级为HelloV3:

F:\hello>go doc rsc.io/quote/v3
package quote // import "rsc.io/quote/v3"

Package quote collects pithy sayings.

func Concurrency() string
func GlassV3() string
func GoV3() string
func HelloV3() string
func OptV3() string

我们可以把在hello.go中的quote.Hello()升级为quote.HelloV3():

package hello

import (
	quoteV3 "rsc.io/quote/v3"
)

func Hello() string {
	return quoteV3.HelloV3()
}

func Proverb() string {
	return quoteV3.Concurrency()
}

这时候,就不需要重命名导入依赖包了,可以把重命名删了:

package hello

import (
	"rsc.io/quote/v3"
)

func Hello() string {
	return quote.HelloV3()
}

func Proverb() string {
	return quote.Concurrency()
}

运行一下测试,确保没有错误:

F:\hello>go test
PASS
ok      hello   0.184s

删除未使用的依赖包

这时候rsc.io/quote已经没有用了,但是它仍然在go list -m all中:

F:\hello>go list -m all
hello
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1

也在go.mod文件中:

module hello

go 1.12

require (
	golang.org/x/text v0.3.2 // indirect
	rsc.io/quote v1.5.2
	rsc.io/quote/v3 v3.1.0
	rsc.io/sampler v1.3.1 // indirect
)

这是为啥呢?因为构建一个单独依赖包,就像使用go buildgo test命令时,可以很容易的判断什么时候缺少什么依赖包,什么时候需要添加,但是无法判断什么时候可以安全删除。只有在检查Module中所有的依赖包以及这些依赖包构建所需要的依赖包的标记之后,才能够删除无用的依赖包。普通的构建命令不会执行这一个步骤,因此无法删除无用的依赖包。

go mod tidy命令就是用来帮助删除这些无用的依赖包的:

F:\hello>go mod tidy

F:\hello>go list -m all
hello
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1

再查看一下go.mod文件:

module hello

go 1.12

require (
	golang.org/x/text v0.3.2 // indirect
	rsc.io/quote/v3 v3.1.0
	rsc.io/sampler v1.3.1 // indirect
)

最后测试一下:

F:\hello>go test
PASS
ok      hello   0.169s

总结

Go Modules 是未来Go的依赖包管理工具,再目前的Go 1.11和Go 1.12中都已经支持。

以下是Go Modules的使用的基本命令:

  • go mod init 创建一个新Modules,初始化go.mod文件。
  • go build, go test 构建命令,添加所需要的依赖包,同时写入go.mod文件。
  • go list -m all 打印当前Modules的依赖包。
  • go get 更改所需依赖包的版本(或添加新的依赖包)。
  • go mod tidy 移除不需要的依赖包。

小伙伴们,把Go Modules用起来吧!

个人站点:https://www.quanquanting.com/

参考

[1]https://blog.golang.org/using-go-modules

[2]https://zhuanlan.zhihu.com/p/59687626

[3]https://semver.org/lang/zh-CN/

你可能感兴趣的:(Golang)