使用GoModule(Using Go Modules译文)

Posted -_-void

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用GoModule(Using Go Modules译文)相关的知识,希望对你有一定的参考价值。

使用GoModule

英文原版:https://blog.golang.org/using-go-modules

介绍

这是系列文章的第一部分

Go1.11和Go1.12包含了初步的GoModule支持,这是一种新的依赖管理系统,它能够更简单精确地管理依赖包的版本信息。本文将介绍使用GoModule所需要的基本操作。

一个module是一个存储在go.mod文件中的GoPackage的集合,go.mod文件定义了这个module的module path(此module被import的根路径)及其所需依赖(其他被此module依赖的module)。每个依赖都以semantic version的形式作为一条module path写入go.mod

在Go1.11时,在$GOPATH/src之外的文件夹或任意父级路径包含go.mod文件时,可以在go命令中使用GoModule。(考虑到兼容性,$GOPATH/src下的文件夹即使存在go.mod文件,也以GOPATH模式运行。)从Go1.13开始module模式会成为默认开发模式。

本文主要内容如下:

  • 新建module
  • 添加依赖
  • 升级依赖
  • 添加新major版本的依赖
  • 升级依赖到新major版本
  • 移除未使用的依赖

新建module

$GOPATH/src之外新建一个文件夹,并在文件夹中创建hello.go

package hello

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

然后再写个hello_test.go

package hello

import "testing"

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

现在这个package还不是module,因为没有go.mod文件,假设当前是在/home/gopher/hello下,运行go test会看到:

$ go test
PASS
ok  	_/home/gopher/hello	0.020s
$

最后一行是全部package的test总结。由于当前既不是在$GOPATH下也不是module模式,而且go命令发现没有用到import,因此当前路径就成了一个“fake”的GOPATH。

现在我们执行go mod init来启用module模式并把当前路径做成module的根路径,然后再执行go test看下效果:

$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello
$ go test
PASS
ok  	example.com/hello	0.020s
$

现在我们就完成了新建module,go mod init命令生成了一个go.mod文件:

$ cat go.mod
module example.com/hello

go 1.12
$

go.mod文件只会出现在module的根目录,子目录的import pathmodule path及子目录的相对路径组成。假设存在子目录world,那么这里就不需要执行go mod init了,这个package会被自动认为是example.com/hello的一部分,其导包路径为example.com/hello/world

添加依赖

GoModule的主要目的就是提高使用他人代码(即添加依赖)的体验。现在在上面的例子中添加一个import:

package hello

import "rsc.io/quote"

func Hello() string 
    return quote.Hello()

然后执行go test:

$ 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  	example.com/hello	0.023s
$

go命令会解析go.mod文件中列出的依赖及其版本,如果发现了go.mod中没有的import,则自动把它的最新版本放进去。(最新版本指的是最新的稳定版或者最新的tag或commit。)本例中go test解析到新的import——rsc.io/quote的最新版本为v1.5.2,以及它用到的两个依赖——rsc.io/samplergolang.org/x/text,只有直接依赖会记录到go.mod:

$ cat go.mod
module example.com/hello

go 1.12

require rsc.io/quote v1.5.2
$

go.modup-to-date的情况下再次执行go test将不会重复此操作,下载过的module会被缓存到$GOPATH/pkg/mod中。

注意虽然go命令方便快捷,但不意味到此结束。现在module严格依赖go.mod中所给出的指定名称、版本。

如上所示,添加一个直接依赖通常会带来其他间接依赖,执行go list -m all可以看到当前依赖的详细信息:

$ go list -m all
example.com/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$

可以看出当前module作为main module总是在第一行出现,然后是按module的path排序的依赖包。

依赖包golang.org/x/text的版本v0.0.0-20170915032832-14c0d48ead0cpseudo-version的一种,这也是对于没有tag情况下的版本语法。

另外go.sum会作为go.mod的附属,包含各个module版本的cryptographic hashes

$ cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZO...
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:Nq...
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3...
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPX...
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/Q...
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9...
$

go命令通过go.sum来确保将来下载的module跟首次下载有相同的校验和,依次确保项目所以来的module未被篡改。go.modgo.sum都应该被包含进版本控制。

升级依赖

在GoModule中,版本通过semantic版本标签来提现。semantic版本标签包含三个部分:major、minor、patch,比如v0.1.2的major是0,minor是1,patch是2。下面看下minor版本升级,下一部分看major版本升级。

go lit -m all的输出中可以看到,当前使用的golang.gor/x/text是没有tag的版本,现在更新到最新tag的版本然后试试是否正常工作:

$ go get golang.org/x/text
go: finding golang.org/x/text v0.3.0
go: downloading golang.org/x/text v0.3.0
go: extracting golang.org/x/text v0.3.0
$ go test
PASS
ok  	example.com/hello	0.013s
$

一切正常,然后看下go list -m all以及go.mod

$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$ cat go.mod
module example.com/hello

go 1.12

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

依赖包golang.org/x/text已经升级到了最新tag版本(v0.3.0),并且go.mod也同步更新。这里的indirect注释表示这个依赖并没有被当前module直接使用,只被其他依赖所依赖。可以通过go help modules查看详细信息。

现在将rsc.io/sampler升级一下minor版本,同样执行go getgo test

$ 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
$ 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	example.com/hello	0.014s
$

报错了,说明新版本的rsc.io/sampler并不兼容当前使用,来看下这个依赖包的可用tag版本列表:

$ 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:

$ go get rsc.io/sampler@v1.3.1
go: finding rsc.io/sampler v1.3.1
go: downloading rsc.io/sampler v1.3.1
go: extracting rsc.io/sampler v1.3.1
$ go test
PASS
ok  	example.com/hello	0.022s
$

注意go get命令中的@v1.3.1显示参数,通常所有go get参数都可以带一个显示版本号,默认为@latest表示最新版本。

添加新主版本的依赖

现在在保重添加一个洗呢func:func Proverb来返回一个go并发谚语,通过调用rsc.io/quote/v3中的quote.Concurrency来实现。首先在hello.go中添加func:

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)
    

然后执行一下go test试试:

$ 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  	example.com/hello	0.024s
$

注意现在依赖了rsc.io/quotersc.io/quote/v3两个module:

$ go list -m rsc.io/q...
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
$

一个module的每个不同的major版本都有不同的导包路径:从v2开始,导包路径必须以major版本结尾。例如v3版本的rsc.io/quote导包路径就是rsc.io/quote/v3。这种习惯称为semantic import versioning,同时它使不同major版本间的不兼容依赖包具有不同的名字。因此,rsc.io/quotev1.6.0v1.5.2两个版本复用了rsc.io/quote。(前面的例子中,rsc.io/sampler v1.99.99应该是向后兼容rsc.io/sampler v1.3.0的,但是bug或其他错误可能会导致这种情况。)

go命令允许在一次构建中每个独立的导包路径包含最多一个版本,也就是说最多存在每个major版本中的一个版本:一个rsc.io/quote,一个rsc.io/quote/v2,一个rsc.io/quote/v3等。这相当于告诉module的作者一个明确的规则:对于一个module来说,不允许同时依赖rsc.io/quote v1.5.2rsc.io/quote v1.6.0,但是允许依赖多个不同的major版本,因为他们的导包路径不同。这使得module的使用者可以增量升级到新的major版本。本例中既需要rsc/quote/v3 v3.1.0中的quote.Concurrency又需要继续使用rsc.io/quote v1.5.2,在比较大的项目中这种增量升级的能力特别重要。

升级依赖到新主版本

现在来完成从rsc.io/quotersc.io/quote/v3的升级,由于major版本变了,我们需要预料到有些API可能有不兼容变更,看过依赖包的doc之后,可以看到Hello变成了HelloV3

$ go doc rsc.io/quote/v3
package quote // import "rsc.io/quote"

Package quote collects pithy sayings.

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

(这里有个已知的bug,所显示的import中丢失了v3
现在将hello.go中的quote.Hello()升级到quoteV3.HelloV3()

package hello

import quoteV3 "rsc.io/quote/v3"

func Hello() string 
    return quoteV3.HelloV3()


func Proverb() string 
    return quoteV3.Concurrency()

到这里就没必要重命名import了:

package hello

import "rsc.io/quote/v3"

func Hello() string 
    return quote.HelloV3()


func Proverb() string 
    return quote.Concurrency()

再来跑一下测试:

$ go test
PASS
ok      example.com/hello       0.014s

移除未使用的依赖

现在已经移除了所有对rsc.io/quote的使用,但是它还在go.mod中,并且执行go list -m all也能看到:

$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
$ cat go.mod
module example.com/hello
go 1.12
require (
    golang.org/x/text v0.3.0 // indirect
    rsc.io/quote v1.5.2
    rsc.io/quote/v3 v3.0.0
    rsc.io/sampler v1.3.1 // indirect
)
$

为什么会这样?因为像go buildgo test这样构建单个包时可以很容易判断缺了什么需要加什么,但是不能确定什么可以被安全移除。仅当检查完module中的所有包并且这些包都整合之后才能移除未使用的依赖。普通的构建命令不会加载此信息,因此它不能安全地删除依赖。

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

$ go mod tidy
$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
$ cat go.mod
module example.com/hello

go 1.12

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

$ go test
PASS
ok  	example.com/hello	0.020s
$

总结

GoModule是go依赖管理的未来,moduel模式在所有已支持的go版本(go1.11与go1.12)都可以使用。
本文介绍了使用GoModule的以下工作流:

  • go mod init创建新的module,同时初始化go.mod
  • build``go test及其他构建命令会按需将依赖添加到go.mod
  • go list -m all查看当前module的依赖
  • go get变更依赖的指定版本(或者添加新的依赖)
  • go mod tidy移除未使用的依赖

我们鼓励开发者在开发中开始使用GoModule并在项目中添加go.modgo.sum

以上是关于使用GoModule(Using Go Modules译文)的主要内容,如果未能解决你的问题,请参考以下文章

发布GoModule(Publishing Go Modules译文)

GoModule:v2及新版本(Go Modules: v2 and Beyond译文)

如何使用go module导入本地包

go module基本使用

golang 之 go module

GO - go mod使用原理