You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

411 lines
13 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 特别放送 | Go Modules实战
你好,我是孔令飞。
今天我们更新一期特别放送作为加餐。在 [特别放送 | Go Modules依赖包管理全讲](https://time.geekbang.org/column/article/416397)中我介绍了Go Modules的知识里面内容比较多你可能还不知道具体怎么使用Go Modules来为你的项目管理Go依赖包。
这一讲我就通过一个具体的案例带你一步步学习Go Modules的常见用法以及操作方法具体包含以下内容
1. 准备一个演示项目。
2. 配置Go Modules。
3. 初始化Go包为Go模块。
4. Go包依赖管理。
## 准备一个演示项目
为了演示Go Modules的用法我们首先需要一个Demo项目。假设我们有一个hello的项目里面有两个文件分别是hello.go和hello\_test.go所在目录为`/home/lk/workspace/golang/src/github.com/marmotedu/gopractise-demo/modules/hello`。
hello.go文件内容为
```go
package hello
func Hello() string {
return "Hello, world."
}
```
hello\_test.go文件内容为
```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
1. 打开Go Modules
确保Go版本`>=go1.11`并开启Go Modules可以通过设置环境变量`export GO111MODULE=on`开启。如果你觉得每次都设置比较繁琐,可以将`export GO111MODULE=on`追加到文件`$HOME/.bashrc`中,并执行 `bash` 命令加载到当前shell环境中。
2. 设置环境变量
对于国内的开发者来说,需要设置`export GOPROXY=https://goproxy.cn,direct`这样一些被墙的包可以通过国内的镜像源安装。如果我们有一些模块存放在私有仓库中也需要设置GOPRIVATE环境变量。
因为Go Modules会请求Go Checksum DatabaseChecksum Database国内也可能会访问失败可以设置`export GOSUMDB=off`来关闭Checksum校验。对于一些模块如果你希望不通过代理服务器或者不校验`checksum`也可以根据需要设置GONOPROXY和GONOSUMDB。
## 初始化Go包为Go模块
3. 创建一个新模块
你可以通过`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`自己推导。下面我来介绍下推导规则。
* 如果有导入路径注释,则使用注释作为模块名,比如:
```bash
package hello // import "github.com/marmotedu/gopractise-demo/modules/hello"
```
则模块名为`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文件
```bash
$ cat go.mod
module github.com/marmotedu/gopractise-demo/modules/hello
go 1.14
```
文件内容表明,当前模块的导入路径为`github.com/marmotedu/gopractise-demo/modules/hello`使用的Go版本是`go 1.14`。
如果要新增子目录创建新的package则package的导入路径自动为 `模块名/子目录名` `github.com/marmotedu/gopractise-demo/modules/hello/<sub-package-name>`,不需要在子目录中再次执行`go mod init`。
比如我们在hello目录下又创建了一个world包`world/world.go`则world包的导入路径为`github.com/marmotedu/gopractise-demo/modules/hello/world`。
## Go包依赖管理
4. 增加一个依赖
Go Modules主要是用来对包依赖进行管理的所以这里我们来给hello包增加一个依赖`rsc.io/quote`
```go
package hello
import "rsc.io/quote"
func Hello() string {
return quote.Hello()
}
```
运行`go test`
```bash
$ 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文件
```bash
module github.com/marmotedu/gopractise-demo/modules/hello
go 1.14
require rsc.io/quote v1.5.2
```
再次执行`go test`
```bash
$ go test
PASS
ok github.com/marmotedu/gopractise-demo/modules/hello 0.003s
```
当我们再次执行`go test`时不会再下载并记录需要的模块因为go.mod目前是最新的并且需要的模块已经缓存到了本地的`$GOPATH/pkg/mod`目录下。可以看到在当前目录还新生成了一个go.sum文件
```bash
$ 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命令。
5. 查看所有依赖模块
我们可以通过`go list -m all`命令查看所有依赖模块:
```bash
$ 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`外,还间接依赖了其他模块。
6. 更新依赖
通过`go list -m all`,我们可以看到模块依赖的`golang.org/x/text`模块版本是`v0.0.0`,我们可以通过`go get`命令,将其更新到最新版本,并观察测试是否通过:
```bash
$ 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文件
```bash
$ 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`并测试:
```bash
$ 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`所有可用的版本,并尝试更新到其他版本:
```bash
$ 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/sampler@v1.3.1
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`还支持多种参数,如下表所示:
![](https://static001.geekbang.org/resource/image/2b/f6/2b80e94c1e91bb18dea9c20695b25bf6.jpg?wh=2248x1496)
7. 添加一个新的major版本依赖
我们尝试添加一个新的函数`func Proverb`,该函数通过调用`rsc.io/quote/v3`的`quote.Concurrency`函数实现。
首先我们在hello.go文件中添加新函数
```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中添加该函数的测试用例
```go
func TestProverb(t *testing.T) {
want := "Concurrency is not parallelism."
if got := Proverb(); got != want {
t.Errorf("Proverb() = %q, want %q", got, want)
}
}
```
然后执行测试:
```bash
$ 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`
```bash
$ go list -m rsc.io/q...
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
```
8. 升级到不兼容的版本
在上一步中,我们使用`rsc.io/quote v1`版本的`Hello()`函数。按照语义化版本规则,如果我们想升级`major`版本,可能面临接口不兼容的问题,需要我们变更代码。我们来看下`rsc.io/quote/v3`的函数:
```bash
$ go doc rsc.io/quote/v3
package quote // import "github.com/google/addlicense/golang/pkg/mod/rsc.io/quote/v3@v3.1.0"
Package quote collects pithy sayings.
func Concurrency() string
func GlassV3() string
func GoV3() string
func HelloV3() string
func OptV3() string
```
可以看到,`Hello()`函数变成了`HelloV3()`这就需要我们变更代码做适配。因为我们都统一模块到一个版本了这时候就不需要再为了避免重名而重命名模块所以此时hello.go内容为
```go
package hello
import (
"rsc.io/quote/v3"
)
func Hello() string {
return quote.HelloV3()
}
func Proverb() string {
return quote.Concurrency()
}
```
执行`go test`
```bash
$ go test
PASS
ok github.com/marmotedu/gopractise-demo/modules/hello 0.003s
```
可以看到测试成功。
9. 删除不使用的依赖
在上一步中,我们移除了`rsc.io/quote`包,但是它仍然存在于`go list -m all`和go.mod中这时候我们要执行`go mod tidy`清理不再使用的依赖:
```bash
$ 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
)
```
10. 使用vendor
如果我们想把所有依赖都保存起来在Go命令执行时不再下载可以执行`go mod vendor`该命令会把当前项目的所有依赖都保存在项目根目录的vendor目录下也会创建`vendor/modules.txt`文件,来记录包和模块的版本信息:
```bash
$ go mod vendor
$ ls
go.mod go.sum hello.go hello_test.go vendor world
```
到这里我就讲完了Go依赖包管理常用的10个操作。
## 总结
这一讲中我详细介绍了如何使用Go Modules来管理依赖它包括以下Go Modules操作
1. 打开Go Modules
2. 设置环境变量;
3. 创建一个新模块;
4. 增加一个依赖;
5. 查看所有依赖模块;
6. 更新依赖;
7. 添加一个新的major版本依赖
8. 升级到不兼容的版本;
9. 删除不使用的依赖。
10. 使用vendor。
## 课后练习
1. 思考下,如何更新项目的所有依赖到最新的版本?
2. 思考下如果我们的编译机器访问不了外网如何通过Go Modules下载Go依赖包
欢迎你在留言区与我交流讨论,我们下一讲见。