0%

【Go语言设计与实现】Go Module

包管理机制

Go 的包管理方式是逐渐演进的, 最初是 monorepo 模式,所有的包都放在 GOPATH 里面,使用类似命名空间的包路径区分包,不过这种包管理显然是有问题,由于包依赖可能会引入破坏性更新,生产环境和测试环境会出现运行不一致的问题。

从 v1.5 开始开始引入 vendor 包模式,如果项目目录下有 vendor 目录,那么 go 工具链会优先使用 vendor 内的包进行编译、测试等,这之后第三方的包管理思路都是通过这种方式来实现,比如说由社区维护准官方包管理工具 dep。

不过官方并不认同这种方式,在 v1.11 中加入了 Go Module 作为官方包管理形式,就这样 dep 无奈的结束了使命。最初的 Go Module 提案的名称叫做 vgo,下面为了介绍简称为 gomod。不过在 v1.11 和 v1.12 的 Go 版本中 gomod 是不能直接使用的。可以通过 go env 命令返回值的 GOMOD 字段是否为空来判断是否已经开启了 gomod,如果没有开启,可以通过设置环境变量 export GO111MODULE=on 开启。

目前 gomod 在 Go v1.12 功能基本稳定,到下一个版本 v1.13 将默认开启,是时候开始在项目中使用 gomod 了。

自从 Go 官方从去年推出 1.11 之后,增加新的依赖管理模块并且更加易于管理项目中所需要的模块。模块是存储在文件树中的 Go 包的集合,其根目录中包含 go.mod 文件。 go.mod 文件定义了模块的模块路径,它也是用于根目录的导入路径,以及它的依赖性要求。每个依赖性要求都被写为模块路径和特定语义版本。

从 Go 1.11 开始,Go 允许在 $GOPATH/src 外的任何目录下使用 go.mod 创建项目。在 $GOPATH/src 中,为了兼容性,Go 命令仍然在旧的 GOPATH 模式下运行。从 Go 1.13 开始,模块模式将成为默认模式。

Go在构建设计方面深受Google内部开发实践的影响,比如go get的设计就深受Google内部单一代码仓库(single monorepo)和基于主干(trunk/mainline based)的开发模型的影响:只获取Trunk/mainline代码和版本无感知。

img{512x368}

Google内部基于主干的开发模型:
– 所有开发人员基于主干trunk/mainline开发:提交到trunk或从trunk获取最新的代码(同步到本地workspace)
– 版本发布时,建立Release branch,release branch实质上就是某一个时刻主干代码的快照;
– 必须同步到release branch上的bug fix和增强改进代码也通常是先在主干上提交(commit),然后再cherry-pick到release branch上

我们知道go get获取的代码会放在$GOPATH/src下面,而go build会在$GOROOT/src和$GOPATH/src下面按照import path去搜索package,由于go get 获取的都是各个package repo的trunk/mainline的代码,因此,Go 1.5之前的Go compiler都是基于目标Go程序依赖包的trunk/mainline代码去编译的。这样的机制带来的问题是显而易见的,至少包括:

  • 因依赖包的trunk的变化,导致不同人获取和编译你的包/程序时得到的结果实质是不同的,即不能实现reproduceable build
  • 因依赖包的trunk的变化,引入不兼容的实现,导致你的包/程序无法通过编译
  • 因依赖包演进而无法通过编译,导致你的包/程序无法通过编译

为了实现reporduceable build,Go 1.5引入了Vendor机制,Go编译器会优先在vendor下搜索依赖的第三方包,这样如果开发者将特定版本的依赖包存放在vendor下面并提交到code repo,那么所有人理论上都会得到同样的编译结果,从而实现reporduceable build。

在Go 1.5发布后的若干年,gopher们把注意力都集中在如何利用vendor解决包依赖问题,从手工添加依赖到vendor、手工更新依赖,到一众包依赖管理工具的诞生:比如: govendorglide以及号称准官方工具的dep,努力地尝试着按照当今主流思路解决着诸如:“钻石型依赖”等难题。

正当gopher认为dep将“顺理成章”地升级为go toolchain一部分的时候,vgo横空出世,并通过对“Semantic Import Versioning”和”Minimal Version Selected”的设定,在原Go tools上简单快速地实现了Go原生的包依赖管理方案 。vgo就是go module的前身。

Quick Start

Example

你可以在 $GOPATH/src 之外的任何地方创建一个新的目录:

1
$ mkdir -p /tmp/scratchpad/repo && cd /tmp/scratchpad/repo

初始化一个新的模块

1
2
$ go mod init github.com/SimpCosm/repo
go: creating new go.mod: module github.com/SimpCosm/repo

成功之后你会发现目录下会生成一个 go.mod 文件。首行为当前的模块名称,接下来是 go 的使用版本。这两行和 npm package.jsonnameengine 字段的功能很类似。

1
2
3
4
$ cat go.mod
module github.com/SimpCosm/repo

go 1.14

创建一个文件 main.go 然后加入以下代码,这里直接 import 了 Go 维护者 Russ Cox 写一个简单的库,

1
2
3
4
5
6
7
8
9
10
package main

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

func main() {
fmt.Println(quote.Hello())
}

编译并且运行

1
2
3
4
5
6
7
8
$ go build -o hello
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
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
$ ./hello
Hello, world.

go build 之后,会在 go.mod 引入所需要的依赖包。之后再来看看 go.mod 文件的情况,require 就是 gin 框架所需要的所有依赖包 并且在每个依赖包的后面已经表明了版本号。

1
2
3
4
5
module github.com/SimpCosm/repo

go 1.14

require rsc.io/quote v1.5.2

与此同时,工程目录下多了一个 go.sum文件,有点类似于 npm package-lock.json

1
2
3
4
5
6
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=

gomod 不会在 $GOPATH/src 目录下保存 rsc.io/quote 包的源码,而是包源码和链接库保存在 $GOPATH/pkg/mod 目录下。

1
2
$ ls $GOPATH/pkg/mod
cache golang.org rsc.io

除了 go run 命令以外,go buildgo test 等命令也能自动下载相关依赖包。

Workflow

可以看到,在上面的示例中完全没有使用 go get 命令,日常的工作流如下:

  • 编写 .go 代码,根据需要添加 import statements
  • go build 或者 go test 命令会自动添加下载新的依赖来满足 import 的要求,并且更新 go.mod 文件
  • 当对依赖的版本有特殊要求的时候,可以使用 go get foo@v1.2.3, go get foo@master, go get foo@e3702bed2 命令,或者直接编辑 go.mod 文件

使用较多的命令初识:

  • go list -m all — 查看在编译中所有直接和非直接依赖的最终版本
  • go list -u -m all — 查看所有直接和非直接依赖可用的升级补丁
  • go get -u ./... or go get -u=patch ./... (from module root directory) — 更新所有直接和非直接以来到最新版本
  • go build ./... or go test ./... (from module root directory) — 编译或测试模块中所有package
  • go mod tidy — 移除不再需要的依赖,并且添加需要的依赖
  • replace directive or gohack — 使用replace 指令
  • go mod vendor — 可选的创造 vendor 目录

New Concept

本小结提供了一些 High-Level的概念介绍。如果想要了解更多的细节,可以看这个40分钟的 Russ Cox 介绍 go module背后的设计哲学 视频,也可以看官方的 Proposal,或者是早期更详细的 vgo博客系列

Modules 定义

go.mod

Version Selection

Semantic Import Versioning

How to Use Modules

包管理命令

升级依赖项

首先我们需要查看以下我们使用到的依赖列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ go list -m all                                                                           ─╯
backend
github.com/davecgh/go-spew v1.1.1
github.com/gin-contrib/sse v0.1.0
github.com/gin-gonic/gin v1.6.3
github.com/go-playground/assert/v2 v2.0.1
github.com/go-playground/locales v0.13.0
github.com/go-playground/universal-translator v0.17.0
github.com/go-playground/validator/v10 v10.2.0
github.com/golang/protobuf v1.3.3
github.com/google/gofuzz v1.0.0
github.com/json-iterator/go v1.1.9
github.com/leodido/go-urn v1.2.0
github.com/mattn/go-isatty v0.0.12
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742
github.com/pmezard/go-difflib v1.0.0
github.com/stretchr/objx v0.1.0
github.com/stretchr/testify v1.4.0
github.com/ugorji/go v1.1.7
github.com/ugorji/go/codec v1.1.7
golang.org/x/sys v0.0.0-20200116001909-b77594299b42
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
gopkg.in/yaml.v2 v2.2.8

因为这里使用的是最新的版本,无法升级,所以这里给出一个回退的例子。将 GIN 框架的版本回退到上个版本。这里需要使用一个命令查看依赖的版本历史。

1
2
3
$ go list -m -versions github.com/gin-gonic/gin
# 将会列出 Gin 版本历史
github.com/gin-gonic/gin v1.1.1 v1.1.2 v1.1.3 v1.1.4 v1.3.0 v1.4.0 v1.5.0 v1.6.0 v1.6.1 v1.6.2 v1.6.3

将版本更新到上个版本,这里只是个演示。

1
2
3
4
5
6
7
8
$ go get github.com/gin-gonic/gin@v1.4.0 # 只需要在依赖后面加上 @version 就可以了
go get github.com/gin-gonic/gin@v1.4.0 ─╯
go: downloading github.com/gin-gonic/gin v1.4.0
go: downloading github.com/json-iterator/go v1.1.9
go: downloading gopkg.in/go-playground/validator.v8 v8.18.2
$ go list -m all
# 看到了版本变化
github.com/gin-gonic/gin v1.4.0

或者可以使用 go mod 来进行版本的切换,这样就需要两个步骤了

1
2
$ go mod edit -require="github.com/gin-gonic/gin@v1.4.0" # 修改 go.mod 文件
$ go mod tidy # 下载更新依赖

go mod tidy 会自动清理掉不需要的依赖项,同时可以将依赖项更新到当前版本。

使用起来这是一个很简单过程,只需要几个命令,你便可以知道依赖的版本信息,以及自由选择安装的版本,一切都变得这么简单。

删除未使用的依赖项

如果你在项目过程需要移除一些不需要的依赖,可以使用下面的命令来执行:

1
$ go mod tidy

命令备忘

1
2
3
4
5
6
7
8
9
10
$ go mod
The commands are:
download download modules to local cache
edit edit go.mod from tools or scripts
graph print module requirement graph
init initialize new module in current directory
tidy add missing and remove unused modules
vendor make vendored copy of dependencies
verify verify dependencies have expected content
why explain why packages or modules are needed

包管理命令

安装依赖

如果要想先下载依赖,那么可以直接像以前那样 go get 即可,不过 gomod 下可以跟语义化版本号,比如 go get foo@v1.2.3,也可以跟 git 的分支或 tag,比如go get foo@master,当然也可以跟 git 提交哈希,比如 go get foo@e3702bed2。需要特别注意的是,gomod 除了遵循语义化版本原则外,还遵循最小版本选择原则,也就是说如果当前版本是 v1.1.0,只会下载不超过这个最大版本号。如果使用 go get foo@master,下次在下载只会和第一次的一样,无论 master 分支是否更新了代码,如下所示,使用包含当前最新提交哈希的虚拟版本号替代直接的 master 版本号。

1
2
3
4
5
6
7
8
9
10
11
12
$ go get golang.org/x/crypto/sha3@master
go: finding golang.org/x/crypto/sha3 latest
go: finding golang.org/x/crypto latest
$ cat go.mod
module github.com/adesight/test

go 1.12

require (
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a // indirect
rsc.io/quote v1.5.2
)

如果下载所有依赖可以使用 go mod download 命令。

升级依赖

查看所有以升级依赖版本:

1
2
3
4
5
6
7
8
9
$ go list -u -m all
go: finding golang.org/x/sys latest
go: finding golang.org/x/crypto latest
github.com/adesight/test
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a [v0.0.0-20190316082340-a2f829d7f35f]
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/sampler v1.99.99

升级次级或补丁版本号:

1
go get -u rsc.io/quote

仅升级补丁版本号:

1
go get -u=patch rscio/quote

升降级版本号,可以使用比较运算符控制:

1
go get foo@'<v1.6.2'

移除依赖

当前代码中不需要了某些包,删除相关代码片段后并没有在 go.mod 文件中自动移出。

运行下面命令可以移出所有代码中不需要的包:

1
go mod tidy

如果仅仅修改 go.mod 配置文件的内容,那么可以运行 go mod edit --droprequire=path,比如要移出 golang.org/x/crypto

1
go mod edit --droprequire=golang.org/x/crypto

查看依赖包

可以直接查看 go.mod 文件,或者使用命令行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ go list -m all
github.com/adesight/test
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/sampler v1.99.99
$ go list -m -json all # json 格式输出
{
"Path": "golang.org/x/text",
"Version": "v0.3.0",
"Time": "2017-12-14T13:08:43Z",
"Indirect": true,
"Dir": "/Users/lishude/go/pkg/mod/golang.org/x/text@v0.3.0",
"GoMod": "/Users/lishude/go/pkg/mod/cache/download/golang.org/x/text/@v/v0.3.0.mod"
}
{
"Path": "rsc.io/quote",
"Version": "v1.5.2",
"Time": "2018-02-14T15:44:20Z",
"Dir": "/Users/lishude/go/pkg/mod/rsc.io/quote@v1.5.2",
"GoMod": "/Users/lishude/go/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.mod"
}

模块配置文本格式化

由于可手动修改 go.mod 文件,所以可能此文件并没有被格式化,使用下面命令进行文本格式化。

1
go mod edit -fmt

发布版本

发布包新版本和其它包管理工具基本一致,可以直接打标签,不过打标签之前需要在 go.mod 中写入相应的版本号:

1
2
3
4
5
6
7
8
9
10
$ go mod edit --module=github.com/islishude/gomodtest/v2
$ cat go.mod
module github.com/islishude/gomodtest/v2

go 1.12

require (
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a // indirect
rsc.io/quote v1.5.2
)

官方推荐将上述过程在一个新分支来避免混淆,那么类如上述例子可以创建一个 v2 分支,但这个不是强制要求的。

还有一种方式发布新版本,那就是在主线版本种加入 v2 文件夹,相应的也需要内置 go.mod 这个文件。

比如上述我们引入的 http://rsc.io/quote 包,其中 v3 版本是用内置文件夹,而 v2 使用的是 tag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ tree .
.
├── LICENSE
├── README.md
├── buggy
│ └── buggy_test.go
├── go.mod
├── go.sum
├── quote.go
├── quote_test.go
└── v3
├── go.mod
├── go.sum
└── quote.go
$ git tag -a
bad
v1.0.0
v1.1.0
v1.2.0
v1.2.1
v1.3.0
v1.4.0
v1.5.0
v1.5.1
v1.5.2
v1.5.3-pre1
v2.0.0
v2.0.1
v3.0.0
v3.1.0
(END)

根据上面的说明,想必你会看到一个问题,当我们升级主版本号的时候,要更改 module 名称,也就是上面所说的加上版本号,这就存在一个问题,如果我们要更新到主版本号的依赖就没有这么简单了,因为升级的依赖包路径都需要修改,这个在其它语言包管理以及 Go 第三方包管理工具都不存在的一点

如下所示,升级 rsc.io/quote 到 v3 版本。注意一点,作为例子这里包作者对函数也加上了版本,其实大部分人是不会加的。这个模式叫做 semantic import versioning,也是备受争议,大多数人认为这个没有特别大的作用,而维护者则认为这是为了 Go 下一个十年的必要条件。

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"

"rsc.io/quote/v3"
)

func main() {
fmt.Println(quote.HelloV3())
}

对于内部开发我觉得还挺好,让大家都了解,不要随意加入破坏性更新。

不过由于这个不讨喜功能,不同版本可以存在同一个包了。补充一句,对于 v0 和 v1 版本并不需要加入到 import path 内。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"

q1 "rsc.io/quote"
"rsc.io/quote/v3"
)

func main() {
fmt.Println(quote.HelloV3())
fmt.Println(q1.Hello())
}

从老项目迁移

从很多第三方的包管理工具迁移到 gomod 特别简单,直接运行 go mod init 即可。

如果没有使用任何第三方包管理工具,除了运行 go mod init 初始化以外,还要使用 go get ./... 下载安装所有依赖包,并更新 go.modgo.sum 文件。

默认情况下,go get 命令使用 @latest 版本控制符对所有依赖进行下载,如果想要更改某一个包的版本,可以使用 go mod edit --require 命令,比如要更新 rsc.io/quote 到 v3.1.0 版本。

1
go mod edit --require=rsc.io/quote@v3.1.0

GOSUMDB

它的值是一个 Go checksum database,用于使 Go 在拉取模块版本时(无论是从源站拉取还是通过 Go module proxy 拉取)保证拉取到的模块版本数据未经篡改,也可以是“off”即禁止 Go 在后续操作中校验模块版本

  • 格式 1:<SUMDB_NAME>+<PUBLIC_KEY>
  • 格式 2:<SUMDB_NAME>+<PUBLIC_KEY> <SUMDB_URL>
  • 拥有默认值:sum.golang.org (之所以没有按照上面的格式是因为 Go 对默认值做了特殊处理)。
  • 可被 Go module proxy 代理 (详见:Proxying a Checksum Database)。
  • sum.golang.org 在中国无法访问,故而更加建议将 GOPROXY 设置为 goproxy.cn,因为 goproxy.cn 支持代理 sum.golang.org

Go Checksum Database

Go checksum database 主要用于保护 Go 不会从任何源头拉到被篡改过的非法 Go 模块版本,其作用(左)和工作机制(右)如下图:

image

如果有兴趣的小伙伴可以看看 Proposal: Secure the Public Go Module Ecosystem,有详细介绍其算法机制,如果想简单一点,查看 go help module-auth 也是一个不错的选择。

GONOPROXY/GONOSUMDB/GOPRIVATE

这三个环境变量都是用在当前项目依赖了私有模块,也就是依赖了由 GOPROXY 指定的 Go module proxy 或由 GOSUMDB 指定 Go checksum database 无法访问到的模块时的场景

  • 它们三个的值都是一个以英文逗号 “,” 分割的模块路径前缀,匹配规则同 path.Match。
  • 其中 GOPRIVATE 较为特殊,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值,所以建议的最佳姿势是只是用 GOPRIVATE。

在使用上来讲,比如 GOPRIVATE=*.corp.example.com 表示所有模块路径以 corp.example.com 的下一级域名 (如 team1.corp.example.com) 为前缀的模块版本都将不经过 Go module proxy 和 Go checksum database,需要注意的是不包括 corp.example.com 本身。

Global Caching

这个主要是针对 Go modules 的全局缓存数据说明,如下:

  • 同一个模块版本的数据只缓存一份,所有其他模块共享使用。
  • 目前所有模块版本数据均缓存在 $GOPATH/pkg/mod$GOPATH/pkg/sum 下,未来或将移至 $GOCACHE/mod$GOCACHE/sum 下( 可能会在当 $GOPATH 被淘汰后)。
  • 可以使用 go clean -modcache 清理所有已缓存的模块版本数据。

另外在 Go1.11 之后 GOCACHE 已经不允许设置为 off 了,我想着这也是为了模块数据缓存移动位置做准备,因此大家应该尽快做好适配。

快速迁移项目至 Go Modules

  • 第一步: 升级到 Go 1.13。
  • 第二步: 让 GOPATH 从你的脑海中完全消失,早一步踏入未来。
    • 修改 GOBIN 路径(可选):go env -w GOBIN=$HOME/bin
    • 打开 Go modules:go env -w GO111MODULE=on
    • 设置 GOPROXY:go env -w GOPROXY=https://goproxy.cn,direct # 在中国是必须的,因为它的默认值被墙了。
  • 第三步(可选): 按照你喜欢的目录结构重新组织你的所有项目。
  • 第四步: 在你项目的根目录下执行 go mod init <OPTIONAL_MODULE_PATH> 以生成 go.mod 文件。
  • 第五步: 想办法说服你身边所有的人都去走一下前四步。

迁移后 go get 行为的改变

  • go help module-getgo help gopath-get分别去了解 Go modules 启用和未启用两种状态下的 go get 的行为

  • 1
    go get

    拉取新的依赖

    • 拉取最新的版本(优先择取 tag):go get golang.org/x/text@latest
    • 拉取 master 分支的最新 commit:go get golang.org/x/text@master
    • 拉取 tag 为 v0.3.2 的 commit:go get golang.org/x/text@v0.3.2
    • 拉取 hash 为 342b231 的 commit,最终会被转换为 v0.3.2:go get golang.org/x/text@342b2e
    • go get -u 更新现有的依赖
    • go mod download 下载 go.mod 文件中指明的所有依赖
    • go mod tidy 整理现有的依赖
    • go mod graph 查看现有的依赖结构
    • go mod init 生成 go.mod 文件 (Go 1.13 中唯一一个可以生成 go.mod 文件的子命令)
  • go mod edit 编辑 go.mod 文件

  • go mod vendor 导出现有的所有依赖 (事实上 Go modules 正在淡化 Vendor 的概念)

  • go mod verify 校验一个模块是否被篡改过

这里我们注意到有两点比较特别,分别是:

  • 第一点:为什么 “拉取 hash 为 342b231 的 commit,最终会被转换为 v0.3.2” 呢。这是因为虽然我们设置了拉取 @342b2e commit,但是因为 Go modules 会与 tag 进行对比,若发现对应的 commit 与 tag 有关联,则进行转换。
  • 第二点:为什么不建议使用 go mod vendor,因为 Go modules 正在淡化 Vendor 的概念,很有可能 Go2 就去掉了。

使用 Go Modules 时常遇见的坑

坑 1: 判断项目是否启用了 Go Modules

image

坑 2: 管理 Go 的环境变量

image

这里主要是提到 Go1.13 新增了 go env -w 用于写入环境变量,而写入的地方是 os.UserConfigDir 所返回的路径,需要注意的是 go env -w 不会覆写。

坑 3: 从 dep、glide 等迁移至 Go Modules

image

这里主要是指从旧有的依赖包管理工具(dep/glide 等)进行迁移时,因为 BUG 的原因会导致不经过 GOPROXY 的代理,解决方法有如下两个:

  • 手动创建一个 go.mod 文件,再执行 go mod tidy 进行补充。
  • 上代理,相当于不使用 GOPROXY 了。

坑 4:拉取私有模块

image

这里主要想涉及两块知识点,如下:

  • GOPROXY 是无权访问到任何人的私有模块的,所以你放心,安全性没问题。
  • GOPROXY 除了设置模块代理的地址以外,还需要增加 “direct” 特殊标识才可以成功拉取私有库。

坑 5:更新现有的模块

image

坑 6:主版本号

image

Go Module Proxy 简介

image

在这里再次强调了 Go Module Proxy 的作用(图左),以及其对应的协议交互流程(图右),有兴趣的小伙伴可以认真看一下。

Goproxy 中国(goproxy.cn)

在这块主要介绍了 Goproxy 的实践操作以及 goproxy.cn 的一些 Q&A 和 近况,如下:

Q&A

Q:如果中国 Go 语言社区没有咱们自己家的 Go Module Proxy 会怎么样?

A:在 Go 1.13 中 GOPROXY 和 GOSUMDB 这两个环境变量都有了在中国无法 访问的默认值,尽管我在 golang.org/issue/31755 里努力尝 试过,但最终仍然无法为咱们中国的 Go 语言开发者谋得一个完美的解决方案。所以从今以后咱 们中国的所有 Go 语言开发者,只要是 使用了 Go modules 的,那么都必须先修改 GOPROXY 和 GOSUMDB 才能正常使用 Go 做开发,否则可能连一个最简单的程序都跑不起 来(只要它有依 赖第三方模 块)。

Q: 我创建 Goproxy 中国(goproxy.cn)的主要原因?

A:其实更早的时候,也就是今年年初我也曾 试图在 golang.org/issue/31020 中请求 Go team 能想办法避免那时的 GOPROXY 即将拥有的默认值可以在中国正常访问,但 Go team 似乎也无能为力,为此我才坚定了创建 goproxy.cn 的信念。既然别人没法儿帮忙,那咱们就 得自己动手,不为别的,就为了让大家以后能够更愉快地使用 Go 语言配合 Go modules 做开发。

最初我先是和七牛云的 许叔(七牛云的 创始人兼 CEO 许式伟)提出了我打算 创建 goproxy.cn 的想法,本是抱着 试试看的目的,但没想 到 许叔几乎是没有超过一分钟的考虑便认可了我的想法并表示愿意一起推 动。那一阵子刚好赶上我在写毕业论文,所以项目开发完后就 一直没和七牛云做交接,一直跑在我的个人服 务器上。直到有一次 goproxy.cn 被攻击了,一下午的功夫 烧了我一百多美元,然后我才 意识到这种项目真不能个人来做。个人来做不靠 谱,万一依赖这个项目的人多了,项目再出什么事儿,那就会给大家􏰁成不必要的损 失。所以我赶紧和七牛云做了交接,把 goproxy.cn 完全交给了七牛云,甚至连域名都过户了去。

近况

image

  • Goproxy 中国 (goproxy.cn) 是目前中国最可靠的 Go module proxy (真不是在自卖自夸)。
  • 为中国 Go 语言开发者量身打􏰁,支持代理 GOSUMDB 的默认值,经过全球 CDN 加速,高可用,可 应用进公司复杂的开发环境中,亦可用作上游代理。
  • 由中国倍受信赖的云服务提供商七牛云无偿提供基础设施支持的开源的非营利性项目。
  • 目标是为中国乃至全世界的 Go 语言开发者提供一个免 费的、可靠的、持 续在线的且经过 CDN 加􏰀的 Go module proxy。
  • 域名已由七牛云进行了备案 (沪ICP备11037377号-56)。

情况

image

此处呈现的是存储大小,主要是针对模块包代码,而一般来讲代码并不会有多大,0-10MB,10-50MB 占最大头,也是能够理解,但是大于 100MB 的模块包代码就比较夸张了。

image

此时主要是展示了一下近期 goproxy.cn 的网络数据情况,我相信未来是会越来越高的,值得期待。

Q&A

Q:如何解决 Go 1.13 在从 GitLab 拉取模块版本时遇到的,Go 错误地按照非期望值的路径寻找目标模块版本结果致使最终目标模块拉取失败的问题?

A:GitLab 中配合 goget 而设置的 <meta> 存在些许问题,导致 Go 1.13 错误地识别了模块的具体路径,这是个 Bug,据说在 GitLab 的新版本中已经被修复了,详细内容可以看 github.com/golang/go/i… 这个 Issue。然后目前的解决办法的话除了升级 GitLab 的版本外,还可以参考 github.com/developer-l… 这条回复。

Q:使用 Go modules 时可以同时依赖同一个模块的不同的两个或者多个小版本(修订版本号不同)吗?

A:不可以的,Go modules 只可以同时依赖一个模块的不同的两个或者多个大版本(主版本号不同)。比如可以同时依赖 example.com/foobar@v1.2.3example.com/foobar/v2@v2.3.4,因为他们的模块路径(module path)不同,Go modules 规定主版本号不是 v0 或者 v1 时,那么主版本号必须显式地出现在模块路径的尾部。但是,同时依赖两个或者多个小版本是不支持的。比如如果模块 A 同时直接依赖了模块 B 和模块 C,且模块 A 直接依赖的是模块 C 的 v1.0.0 版本,然后模块 B 直接依赖的是模块 C 的 v1.0.1 版本,那么最终 Go modules 会为模块 A 选用模块 C 的 v1.0.1 版本而不是模块 A 的 go.mod 文件中指明的 v1.0.0 版本。

这是因为 Go modules 认为只要主版本号不变,那么剩下的都可以直接升级采用最新的。但是如果采用了最新的结果导致项目 Break 掉了,那么 Go modules 就会 Fallback 到上一个老的版本,比如在前面的例子中就会 Fallback 到 v1.0.0 版本。

Q:在 go.sum 文件中的一个模块版本的 Hash 校验数据什么情况下会成对出现,什么情况下只会存在一行?

A:通常情况下,在 go.sum 文件中的一个模块版本的 Hash 校验数据会有两行,前一行是该模块的 ZIP 文件的 Hash 校验数据,后一行是该模块的 go.mod 文件的 Hash 校验数据。但是也有些情况下只会出现一行该模块的 go.mod 文件的 Hash 校验数据,而不包含该模块的 ZIP 文件本身的 Hash 校验数据,这个情况发生在 Go modules 判定为你当前这个项目完全用不到该模块,根本也不会下载该模块的 ZIP 文件,所以就没必要对其作出 Hash 校验保证,只需要对该模块的 go.mod 文件作出 Hash 校验保证即可,因为 go.mod 文件是用得着的,在深入挖取项目依赖的时候要用。

Q:能不能更详细地讲解一下 go.mod 文件中的 replace 动词的行为以及用法?

A:这个 replace 动词的作用是把一个“模块版本”替换为另外一个“模块版本”,这是“模块版本”和“模块版本(module path)”之间的替换,“=>”标识符前面的内容是待替换的“模块版本”的“模块路径”,后面的内容是要替换的目标“模块版本”的所在地,即路径,这个路径可以是一个本地磁盘的相对路径,也可以是一个本地磁盘的绝对路径,还可以是一个网络路径,但是这个目标路径并不会在今后你的项目代码中作为你“导入路径(import path)”出现,代码里的“导入路径”还是得以你替换成的这个目标“模块版本”的“模块路径”作为前缀。

另外需要注意,Go modules 是不支持在 “导入路径” 里写相对路径的。举个例子,如果项目 A 依赖了模块 B,比如模块 B 的“模块路径”是 example.com/b,然后它在的磁盘路径是 ~/b,在项目 A 里的 go.mod 文件中你有一行 replace example.com/b=>~/b,然后在项目 A 里的代码中的“导入路基”就是 import"example.com/b",而不是 import"~/b",剩下的工作是 Go modules 帮你自动完成了的。

然后就是我在分享中也提到了, exclude 和 replace 这两个动词只作用于当前主模块,也就是当前项目,它所依赖的那些其他模块版本中如果出现了你待替换的那个模块版本的话,Go modules 还是会为你依赖的那个模块版本去拉取你的这个待替换的模块版本。

举个例子,比如项目 A 直接依赖了模块 B 和模块 C,然后模块 B 也直接依赖了模块 C,那么你在项目 A 中的 go.mod 文件里的 replace c=>~/some/path/c 是只会影响项目 A 里写的代码中,而模块 B 所用到的还是你 replace 之前的那个 c,并不是你替换成的 ~/some/path/c 这个。

参考资料