Skip to content

Latest commit

 

History

History
416 lines (267 loc) · 13.2 KB

54.2 特别放送 Go Modules实战.md

File metadata and controls

416 lines (267 loc) · 13.2 KB

特别放送 | Go Modules实战

今天我们更新一期特别放送作为加餐。在 特别放送 | Go Modules 依赖包管理全讲中,我介绍了 Go Modules 的知识,里面内容比较多,你可能还不知道具体怎么使用 Go Modules 来为你的项目管理 Go 依赖包。

这一讲,我就通过一个具体的案例,带你一步步学习 Go Modules 的常见用法以及操作方法,具体包含以下内容:

准备一个演示项目。

配置 Go Modules。

初始化 Go 包为 Go 模块。

Go 包依赖管理。

准备一个演示项目

为了演示 Go Modules 的用法,我们首先需要一个 Demo 项目。假设我们有一个 hello 的项目,里面有两个文件,分别是 hello.go 和 hello_test.go,所在目录为/home/lk/workspace/golang/src/github.com/marmotedu/gopractise-demo/modules/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)
  }
}

这时候,该目录包含了一个 Go 包,但还不是 Go 模块,因为没有 go.mod 件。接下来,我就给你演示下,如何将这个包变成一个 Go 模块,并执行 Go 依赖包的管理操作。这些操作共有 10 个步骤,下面我们来一步步看下。

配置 Go Modules

打开 Go Modules

确保 Go 版本>=go1.11,并开启 Go Modules,可以通过设置环境变量export GO111MODULE=on开启。如果你觉得每次都设置比较繁琐,可以将export GO111MODULE=on追加到文件$HOME/.bashrc中,并执行 bash 命令加载到当前 shell 环境中。

设置环境变量

对于国内的开发者来说,需要设置export GOPROXY=https://goproxy.cn,direct,这样一些被墙的包可以通过国内的镜像源安装。如果我们有一些模块存放在私有仓库中,也需要设置 GOPRIVATE 环境变量。

因为 Go Modules 会请求 Go Checksum Database,Checksum Database 国内也可能会访问失败,可以设置export GOSUMDB=off来关闭 Checksum 校验。对于一些模块,如果你希望不通过代理服务器,或者不校验checksum,也可以根据需要设置 GONOPROXY 和 GONOSUMDB。

初始化 Go 包为 Go 模块

创建一个新模块

你可以通过go mod init命令,初始化项目为 Go Modules。 init 命令会在当前目录初始化并创建一个新的 go.mod 文件,也代表着创建了一个以项目根目录为根的 Go Modules。如果当前目录已经存在 go.mod 文件,则会初始化失败。

在初始化 Go Modules 时,需要告知go mod init要初始化的模块名,可以指定模块名,例如go mod init github.com/marmotedu/gopractise-demo/modules/hello。也可以不指定模块名,让init自己推导。下面我来介绍下推导规则。

如果有导入路径注释,则使用注释作为模块名,比如:


package hello // import "github.com/marmotedu/gopractise-demo/modules/hello"

如果没有导入路径注释,并且项目位于 GOPATH 路径下,则模块名为绝对路径去掉$GOPATH/src后的路径名,例如GOPATH=/home/lk/workspace/golang,项目绝对路径为/home/colin/workspace/golang/src/github.com/marmotedu/gopractise-demo/modules/hello,则模块名为github.com/marmotedu/gopractise-demo/modules/hello。

初始化完成之后,会在当前目录生成一个 go.mod 文件:


$ cat go.mod
module github.com/marmotedu/gopractise-demo/modules/hello

go 1.14

如果要新增子目录创建新的 package,则 package 的导入路径自动为 模块名/子目录名 :github.com/marmotedu/gopractise-demo/modules/hello/,不需要在子目录中再次执行go mod init。

比如,我们在 hello 目录下又创建了一个 world 包world/world.go,则 world 包的导入路径为github.com/marmotedu/gopractise-demo/modules/hello/world。

Go 包依赖管理

增加一个依赖

Go Modules 主要是用来对包依赖进行管理的,所以这里我们来给 hello 包增加一个依赖rsc.io/quote:


package hello

import "rsc.io/quote"

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

运行go test:


$ go test
go: finding module for package rsc.io/quote
go: downloading rsc.io/quote v1.5.2
go: found rsc.io/quote in rsc.io/quote v1.5.2
go: downloading rsc.io/sampler v1.3.0
PASS
ok    github.com/google/addlicense/golang/src/github.com/marmotedu/gopractise-demo/modules/hello  0.003s

当 go 命令在解析源码时,遇到需要导入一个模块的情况,就会去 go.mod 文件中查询该模块的版本,如果有指定版本,就导入指定的版本。

如果没有查询到该模块,go 命令会自动根据模块的导入路径安装模块,并将模块和其最新的版本写入 go.mod 文件中。在我们的示例中,go test将模块rsc.io/quote解析为rsc.io/quote v1.5.2,并且同时还下载了rsc.io/quote模块的两个依赖模块:rsc.io/quote和rsc.io/sampler。只有直接依赖才会被记录到 go.mod 文件中。

查看 go.mod 文件:


module github.com/marmotedu/gopractise-demo/modules/hello

go 1.14

require rsc.io/quote v1.5.2

再次执行go test:


$ go test
PASS
ok    github.com/marmotedu/gopractise-demo/modules/hello  0.003s

当我们再次执行go test时,不会再下载并记录需要的模块,因为 go.mod 目前是最新的,并且需要的模块已经缓存到了本地的$GOPATH/pkg/mod目录下。可以看到,在当前目录还新生成了一个 go.sum 文件:


$ cat go.sum
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 test在执行时,还可以添加-mod选项,比如go test -mod=vendor。-mod有 3 个值,我来分别介绍下。

readonly:不更新 go.mod,任何可能会导致 go.mod 变更的操作都会失败。通常用来检查 go.mod 文件是否需要更新,例如用在 CI 或者测试场景。

vendor:从项目顶层目录下的 vendor 中导入包,而不是从模块缓存中导入包,需要确保 vendor 包完整准确。

mod:从模块缓存中导入包,即使项目根目录下有 vendor 目录。

如果go test执行时没有-mod选项,并且项目根目录存在 vendor 目录,go.mod 中记录的 go 版本大于等于1.14,此时go test执行效果等效于go test -mod=vendor。-mod标志同样适用于 go build、go install、go run、go test、go list、go vet 命令。

查看所有依赖模块

我们可以通过go list -m all命令查看所有依赖模块:


$ go list -m all
github.com/marmotedu/gopractise-demo/modules/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外,还间接依赖了其他模块。

更新依赖

通过go list -m all,我们可以看到模块依赖的golang.org/x/text模块版本是v0.0.0,我们可以通过go get命令,将其更新到最新版本,并观察测试是否通过:


$ go get golang.org/x/text
go: golang.org/x/text upgrade => v0.3.3
$ go test
PASS
ok    github.com/marmotedu/gopractise-demo/modules/hello  0.003s

go test命令执行后输出 PASS 说明升级成功,再次看下go list -m all和 go.mod 文件:


$ go list -m all
github.com/marmotedu/gopractise-demo/modules/hello
golang.org/x/text v0.3.3
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$ cat go.mod
module github.com/marmotedu/gopractise-demo/modules/hello

go 1.14

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

可以看到,golang.org/x/text包被更新到最新的 tag 版本 (v0.3.3),并且同时更新了 go.mod 文件。// indirect说明golang.org/x/text是间接依赖。现在我们再尝试更新rsc.io/sampler并测试:


$ go get rsc.io/sampler
go: rsc.io/sampler upgrade => v1.99.99
go: downloading 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  github.com/marmotedu/gopractise-demo/modules/hello  0.004s

测试失败,说明最新的版本v1.99.99与我们当前的模块不兼容,我们可以列出rsc.io/sampler所有可用的版本,并尝试更新到其他版本:


$ 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: downloading rsc.io/sampler v1.3.1
$ go test
PASS
ok    github.com/marmotedu/gopractise-demo/modules/hello  0.004s

可以看到,更新到v1.3.1版本,测试是通过的。go get还支持多种参数,如下表所示:

img

添加一个新的 major 版本依赖

我们尝试添加一个新的函数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)
    }
}

然后执行测试:


$ go test
go: finding module for package rsc.io/quote/v3
go: found rsc.io/quote/v3 in rsc.io/quote/v3 v3.1.0
PASS
ok    github.com/marmotedu/gopractise-demo/modules/hello  0.003s

测试通过,可以看到当前模块同时依赖了同一个模块的不同版本rsc.io/quote和rsc.io/quote/v3:


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

升级到不兼容的版本

在上一步中,我们使用rsc.io/quote v1版本的Hello()函数。按照语义化版本规则,如果我们想升级major版本,可能面临接口不兼容的问题,需要我们变更代码。我们来看下rsc.io/quote/v3的函数:


$ go doc rsc.io/quote/v3
package quote // import "github.com/google/addlicense/golang/pkg/mod/rsc.io/quote/[email protected]"

Package quote collects pithy sayings.

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

可以看到,Hello()函数变成了HelloV3(),这就需要我们变更代码做适配。因为我们都统一模块到一个版本了,这时候就不需要再为了避免重名而重命名模块,所以此时 hello.go 内容为:


package hello

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

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

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

执行go test:


$ go test
PASS
ok    github.com/marmotedu/gopractise-demo/modules/hello  0.003s

可以看到测试成功。

删除不使用的依赖

在上一步中,我们移除了rsc.io/quote包,但是它仍然存在于go list -m all和 go.mod 中,这时候我们要执行go mod tidy清理不再使用的依赖:


$ go mod tidy
[colin@dev hello]$ cat go.mod
module github.com/marmotedu/gopractise-demo/modules/hello

go 1.14

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

使用 vendor

如果我们想把所有依赖都保存起来,在 Go 命令执行时不再下载,可以执行go mod vendor,该命令会把当前项目的所有依赖都保存在项目根目录的 vendor 目录下,也会创建vendor/modules.txt文件,来记录包和模块的版本信息:


$ go mod vendor
$ ls
go.mod  go.sum  hello.go  hello_test.go  vendor  world

到这里,我就讲完了 Go 依赖包管理常用的 10 个操作。

总结

这一讲中,我详细介绍了如何使用 Go Modules 来管理依赖,它包括以下 Go Modules 操作:

打开 Go Modules;

设置环境变量;

创建一个新模块;

增加一个依赖;

查看所有依赖模块;

更新依赖;

添加一个新的 major 版本依赖;

升级到不兼容的版本;

删除不使用的依赖。

使用 vendor。

课后练习

思考下,如何更新项目的所有依赖到最新的版本?

思考下,如果我们的编译机器访问不了外网,如何通过 Go Modules 下载 Go 依赖包?