Go 包管理的进化史 —— Modules

经历了 Go 的 workspace,然后又有 Vendor,Go 终于有了更加好的包依赖解决方案 Modules 机制。

快速入门

go modules 官方文档

即使从 go1.5 引入了 vendor 机制和配合官方的 dep 工具,也依然不是一个便捷的解放方案。
不过现在 go modules 随着 golang1.11 的发布而和我们见面了,这是官方提倡的新的包管理,乃至项目管理机制。

因此项目结构可以像下面一样的:

1
2
3
4
5
6
7
8
$GOPATH
├── bin
├── pkg
├── src
└── ProjectA // 项目不再放在 src 目录中

ProjectB // 甚至可以不用放在 $GOPATH 中,
ProjectZ

从项目结构来看,就是 Go 的 workspace 不再像以前规定得那么死。

在 go1.11 中,Modules 还只是一个实验性功能,
所以 go 提供了一个环境变量 “GO111MODULE”,默认值为 auto,
如果当前目录里有 go.mod 文件,就使用 go modules,否则使用旧的 $GOPATH 和 vendor 机制,
从 go1.12 开始,应该会强制使用 go modules 不再使用 $GOPATH 和 vendor 机制。

试试看吧:

1
...\ProjectZ> go mod init test
1
2
3
4
5
6
7
$GOPATH
├── bin
├── pkg
└── src

ProjectZ
└── go.mod // 自动生成的文件,里面只有一行 module test

接下来,我们手动创建一个 main.go 文件,内容是:

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

import "github.com/gin-gonic/gin"

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}

运行看看:

1
...\ProjectZ> go run main.go

这个时候会下载相应的依赖包,如果你遇到恶心的墙网络问题,那么可以添加环境变量:

详情,可以浏览 https://goproxy.io

First, you will need to enable the Go Modules feature and configure Go to use the proxy.

墙内阿里的代理:https://mirrors.aliyun.com/goproxy

因为我们的文件夹中有 go.mod,所以可以不用添加环境变量: GO111MODULE = on

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$GOPATH
├── bin
├── pkg
│ └── cache
│ └── mod
│ └── github.com
│ └── gin-gonic
│ └── gin@v1.3.0 // gin 带了版本,也不像以前先要手动用 go get 获取一次
└── src

ProjectZ
├── go.mod // 依赖信息,需要添加到 svn,git 等版本控制系统。
├── go.sum // 校验信息,需要添加到 svn,git 等版本控制系统。
└── main.go //

看看 go.mod 文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
module test

require (
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect
github.com/gin-gonic/gin v1.3.0
github.com/golang/protobuf v1.2.0 // indirect
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect
github.com/mattn/go-isatty v0.0.4 // indirect
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect(间接的,gin 包依赖其他包)
)

看看 go.sum 文件的内容:

1
2
3
4
5
6
7
8
9
10
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=

...

以前我们可以在控制台任何目录下用 go get,那么会下载到 src 目录中。

1
> go get github.com/gin-gonic/gin

假设我们要切换到 gin@1.1.4 版本

1
2
>go get github.com/gin-gonic/gin@1.1.4
go: cannot use path@version syntax in GOPATH mode

必须切换到有 go.mod 文件的目录。

1
...\ProjectZ> go get github.com/gin-gonic/gin@1.1.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$GOPATH
├── bin
├── pkg
│ └── cache
│ └── mod
│ └── github.com
│ └── gin-gonic
│ └── gin@v1.1.4 // 版本
│ └── gin@v1.3.0 // 版本
└── src

ProjectZ
├── go.mod
├── go.sum
└── main.go

同时留意 go.modgo.sum 文件内容的变化。

除了用 go get 来切换版本,还可以通过编辑 go.mod 的文件内容,然后

1
...\ProjectZ> go mod tidy

也会下载相应的版本和该版本需要的依赖包。

下面是可能用到的功能:

  • go list -m all —— 查看将在生成中用于所有直接和间接依赖关系的最终版本
  • go list -u -m all —— 查看所有直接和间接依赖项的可用次要和修补程序升级
  • go get -u or go get -u=patch —— 更新所有直接和间接依赖关系, 以进行最新的次要或修补程序升级 (忽略预发布)
  • go build ./... or go test ./... ——
  • go mod download —— 下载依赖的module到本地cache
  • go mod edit —— 编辑go.mod文件
  • go mod graph —— 打印模块依赖图
  • go mod init —— 再当前文件夹下初始化一个新的module, 创建go.mod文件
  • go mod tidy —— 增加丢失的module,去掉未用的module
  • go mod vendor —— 将依赖复制到vendor下
  • go mod verify —— 校验依赖
  • go mod why —— 解释为什么需要依赖

最后你会发现怎么 pkg 文件夹中没有生成 xxx.a 包的文件呢?
可以用 go env 查看,有一个叫 GOCACHE 的变量

小结

可以看出 $GOPATH 从以前存放引用包和项目的地方,变成只存放包,项目单独出来了,
而依然有 vendor 是有时候我们需要将引用包一起放到 svn 上。

概念

Modules

module (模块)是将一组相关的 Go packages(包),当作一个单元进行版本控制。

module (模块)精准地记录着必要的依赖关系,并且可用于构建项目。

通常,一个版本控制存储库只在在存储库根目录中定义的一个模块。
(单个存储库中支持多个模块, 但通常会比每个存储库的单个模块需要更多的工作)

总结一下 repositories(存储库), modules(模块), 和 packages(包) 之间的关系:

  • 一个 repository(存储库) 包含 一个或以上 的 Go 模块
  • 每个 module(模块) 包含 一个或以上 的 Go 包
  • 每个 package(包) 包含 一个或以上 的 Go 源码文件
1
2
3
4
5
6
repo            // 存储库——项目文件夹
├── go.mod // 模块
├── bar // 包
│ └── bar.go // Go 源码文件
└── foo
└── foo.go

模块必须进行语义版本控制,规则是 v(major[主版本号]).(minor[次版本号]).(patch[补丁版本号]),
如 v0.1.0, v1.2.3, or v1.5.0-rc.1,开头的 v 是必须的。
如果是使用 Git,则标记发布的提交及其版本。公共和私有模块存储库和代理正在变得可用。

go.mod

A module is defined by a tree of Go source files with a go.mod file in the tree’s root directory.
Module source code may be located outside of GOPATH.
Module 源码可以位于 GOPATH 目录之外。

通常,每个存储库会有一个 go.mod 位于 存储库 的根目录,但 go.mod 可以位于其它位置。

以下是一个 go.mod 文件定义 github.com/my/thing 模块的示例:

1
2
3
4
5
6
module github.com/my/thing

require (
github.com/some/dependency v1.2.3
github.com/another/dependency/v4 v4.0.0
)

有四个指令:module(模块)require(依赖)replace(代替)exclude(排除)

模块通过模块指令在其 go. mod 中声明其标识, 该指令提供模块路径
模块中所有包的导入(import)路径将模块路径共享为通用前缀。
模块路径和从 go. mod 到包目录的相对路径共同确定了包的导入(import)路径。

例如,如果你创建一个叫 github.com/my/repo 的模块,
它包含了两个包,它们的导入(import)路径分别是 github.com/my/repo/foogithub.com/my/repo/bar
那么通常在 go.mod 文件中的第一行声明你的模块路径,如 module github.com/my/repo,相应在硬盘上的文件结构如下:

1
2
3
4
5
6
repo/            
├── go.mod // 在 go.mod 文件的第一行声明:module github.com/my/repo
├── bar // 其他使用者要导入本包的路径是:github.com/my/repo/bar
│ └── bar.go
└── foo // 其他使用者要导入本包的路径是:github.com/my/repo/foo
└── foo.go

在 Go 源码中,包的导入是使用模块路径的完整路径。
例如,如果一个模块在它的go.mod声明了 module example.com/my/module,那么使用者则:

1
import "example.com/my/module/mypkg"

导入的包 mypkg 来自模块 example.com/my/module

exclude(排除)replace(代替) 指令仅作用于当前主模块中。
如果 exclude(排除)replace(代替) 在其他模块中,那么构建(build)主模块时,会忽略它们。
因此, exclude(排除)replace(代替) 语句允许主模块完全控制自己的生成, 而不完全受依赖关系的控制。
具体详情,请看 官方例子

版本选择

如果你在你的源码中添加了一行新的 import,那么在 go.modrequire 指令处中不会立刻覆盖。
大部分 go 命令行,如 go buildgo test 会自动查找准确的模块,
并添加它的最高版本作为直接依赖项添加到你的模块 go.mod 文件中 require 指令处。

如果你的模块依赖 模块A 和 模块B,
其中 模块A 有 require D v1.0.0,而 模块B 有 require D v1.1.1
那么根据最小版本选择 算法,在构建(build)你的模块的时候会选择 D v1.1.1

如何准备发布

运行 go mod tidy 删减无关的依赖 和 确保你的 go.mod 刷新所有可能用于构建 tags/OS/架构组合。

运行 go test all 测试模块, 以验证当前选定的包版本是否兼容。

确保你的 go.mod 和 go.sum 一起提交到版本管理系统中(svn,git)

结束语

本来想尝试翻译官方文档,但发现自己战五渣英语水平好吃力,所以选择只翻译重点的。
关于怎么将之前的版本依赖迁移到 Module 机制上 和 FAQ,请看 go modules 官方文档

觉得文章对您有帮助,请我喝瓶肥宅快乐水可好 (๑•̀ㅂ•́)و✧