52

深入浅出Go Modules // 指尖时光

 5 years ago
source link: https://blog.caojun.xyz/posts/gomodules/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Go语言从1.11和1.12版本开始引入了初步的modules支持,官方计划是从1.13版本开始默认支持module模式。按Go官方团队的里程碑计划估计,今年8月份1.13会正式发布。

Module是什么

“A module is a collection of Go packages”, 一个模块是一个Go package的集合。

package是什么呢?

如下代码段所示:

import (
  "fmt"
  log "github.com/sirupsen/logrus"
)

fmtgithub.com/sirupsen/logrus 都是package,其中fmt是语言本身包含的package(可以类比为C++的STL),logrus是第三方package。

为什么需要package?

package的引入是为了支持模块化的源代码组织方式,Go程序至少需要一个main package,但应该几乎难以找到一个有用的稍具规模的程序可以不用引入其它package的。

Module的用途

我们思考一个问题,如果项目规模较大,我们引入了第三方包A(v2.0.0)和B (V1.1.0),而B-1.0.0又引入了A-1.0.0。我们通过go get获取的包A的版本只能是确定的一个版本,它不可能同时是v2.0.0和v1.0.0,这时候就需要依赖管理(dependency management) 了。Module的主要工作就是进行依赖管理,在这之前Go语言有一些第三方的依赖管理工具,例如godep, govendor等等。可以说,依赖管理是Go工程的刚需工具,那么在语言官方层面上统一规范是提供支持就是非常有必要的了。

语义化版本

依赖管理处理的其中一个核心问题就是版本升级,如果要在语言官方层面统一依赖管理,那么版本在package的版本管理上统一规范就是很必要的了,Go Module要求遵从语义化的版本规范,关于版本选择的设计详细,可以参考Version Selection

使用Module

读者如果是Module初学者,可以同步在自己的电脑上进行操作。

在命令行中输入go mod help即可查看Module工具有哪些命令。

download edit graph init tidy vendor verify why

因为Module目前还不是语言默认的依赖管理模式(要到Go1.13),所以还需要考虑兼容问题。如果在GOPATH下,Module模式是默认关闭的(即使项目有go.mod文件),需要通过GO111MODULE=on显式的打开。如果不在GOPATH下,则不需要设置该环境变量激活Module模式。另一方面,有Module支持,我们的项目不用统一放到一个GOPATH下或者设置多个GOPATH了。Module管理的模块依赖文件保存在 GOPATH/pkg/mod 目录下。GOPATH/pkg/mod/cache 目录是缓存目录,防止重复下载,其它目录则组织不同版本的模块,例如:

golang.org/x/

[email protected]
[email protected]
[email protected]
[email protected]
[email protected]

假设我们在GOPATH外新建了hello目录,其中有hello.go和hello_test.go文件。文件内容分别如下:

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)
    }
}

进入到hello目录,运行go mod init example.com/hello命令将该目录作为一个module的根目录进行初始化,初始化结束后hello目录新增了go .mod文件,文件内容为:

module example.com/hello
go 1.12

表示在该根目录声明了 example.com/hello 模块,使用的是Go版本是1.12,此后如果要新增子目录创建新的package,则package的导入路径自动为module名加子目录名。

创建morning/morning.go, 在hello.go中导入该package的路径为 import example.com/hello/morning

我们修改hello.go导入一个外部模块:

package hello

import "rsc.io/quote"

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

运行go test,第一次运行可能会比较慢,因为需要先下载相关文件。PASS后,查看go.mod文件:

module example.com/hello

go 1.12

require rsc.io/quote v1.5.2

我们可以看到在文件末尾新增了一条依赖声明,定义了依赖的模块(rsc.io/quote)及其版本号(v1.5.2)。还可以看到新增了一个go.sum文件,这是一个校验文件,不需要人工维护,是工具用于判断依赖是否发生变化。

查看所有依赖模块

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

该命令列出当前模块依赖的所有模块,可以看出,除了 rsc.io/quote v1.5.2 外,还间接依赖其它模块(rsc.io/quote v1.5.2所依赖的)。

升级到最新版本

通过上一个命令,我们知道当前模块依赖的golang.org/x/text模块版本是v0.0.0,我们想尝试一下将它升级到最新版本是否兼容。运行: go get golang.org/x/text

升级后的go.mod文件:

module example.com/hello

go 1.12

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

升级后,运行go test,PASS代表升级成功。

我们再升级一下rsc.io/sampler,go get rsc.io/sampler,再运行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.006s

升级失败,说明升级到最新的rsc.io/sampler与当前模块不兼容,如果rsc.io/sampler严格遵守语义化版本]1规范,则我们升级到一个兼容版本v1.3.z (z > 0)是应该可以通过测试的。

升级到指定版本

我们先看看它有哪些已发布版本: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.1: go get rsc.io/[email protected], 继续测试go test, 测试通过!再检查go.mod文件:

module example.com/hello

go 1.12

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

添加一个新的major版本依赖

当遇到依赖同一模块的不同版本需求时,我们可以这样解决:

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

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)
    }
}

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

因为是同一个模块,所以我们将v3版本导入为quoteV3。运行go test,go.mod文件有如下变更:

module example.com/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
)

列出当前当前模块(example.com/hello)依赖的rsc.io/quote模块的所有版本:

go list -m rsc.io/q..., 结果为:

rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0

升级到不兼容的版本

按照语义化版本规则,major版本的变更,意味着接口不兼容。如果还要使用新版本依赖,那就要求我们修改自己的代码去显式的使用新版本。这里,我们将模块对rsc.io/quote的所有依赖都升级到v3,删除对低版本的依赖,修改hello.go:

package hello

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

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

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

运行go test, PASS。但是查看go.mod文件,依然包含 rsc.io/quote v1.5.2 ,此时我们运行go mod tidy命令清理不再使用的依赖,清理后再查看go.mod文件,已经不再包含 rsc.io/quote v1.5.2

vendor

运行go mod vendor命令将在当前模块根目录生成vendor目录,将module的依赖都拷贝到该目录下。这样做主要有两个好处:

  1. 保证依赖的所有模块都可以重复获取到,例如防止第三方作者将自己的开源项目删除。
  2. 提高CI工具的效率,有了vendor目录,go build时就不用再重新去下载。

当然,这也是有坏处的,那就是如果将vendor目录也提交到版本控制中,则会增加项目大小,增加管理复杂度。

发布Module

如果要将自己的module发布供他人使用,则需要遵守一定的规范,具体参考Go官方文档

go.mod和go.sum文件都应该纳入到版本控制系统中,Go Module要依赖这两个文件工作。Go Modules核心要解决的问题就是可靠的重复构建,这是一个确定的工程需求。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK