using go module

Using Go Modules》

原文重点是:Go Module 不是简单替代 GOPATH,而是把“依赖版本”变成项目的一部分,由 go.mod 显式声明,由 go.sum 保证内容一致性。

1. Module 是什么

一个 module 是一组 Go packages 的集合。

它的根目录下有一个 go.mod 文件:

module example.com/hello

go 1.12

go.mod 主要记录两类信息:

  • module path: 当前模块的路径,也就是项目根目录的 import path,如果需要 import 项目其他目录,就从这个 import path 开始。
  • requirements: 构建当前模块所需的依赖模块及其版本

例如:

require rsc.io/quote v1.5.2

含义是:当前模块依赖 rsc.io/quote,版本固定为 v1.5.2

2. 创建一个新 module

假设有一个包:

package hello

func Hello() string {
    return "Hello, world."
}

初始化 module:

go mod init example.com/hello

生成:

module example.com/hello

go 1.12

此时当前目录就是 module root。

子目录不需要再执行 go mod init。例如:

example.com/hello/world

会自动被视为当前 module 下的 package。

核心点:

  • 一个仓库/项目通常只有一个 go.mod,放在 module root。
  • 子目录 package 的 import path = module path + 子目录路径。

3. 添加依赖

当代码中新增 import:

import "rsc.io/quote"

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

执行:

go test

Go 会自动:

  1. 发现缺少 rsc.io/quote
  2. 查询包含该 package 的 module
  3. 下载合适版本
  4. 更新 go.mod
  5. 更新 go.sum

生成的 go.mod 可能是:

module example.com/hello

go 1.12

require rsc.io/quote v1.5.2

注意:go.mod 主要记录直接依赖。

从 Go 1.16 开始,自动下载行为改变了。
go build、go test 发现缺少依赖时,一般不会直接下载和修改 go.mod,而是报错并提示你执行类似命令:go get rsc.io/quote

查看完整依赖图:

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

第一行永远是当前 module,也叫 main module

4. go.sum 的作用

go.sum 不是锁文件的简单等价物。

它记录的是:某个 module 版本内容的加密哈希。作用是防止依赖内容被意外篡改。

例如:

$ cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZO...
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:Nq...
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3...
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPX...
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/Q...
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9...
$

go 命令使用 go.sum 文件来确保后续下载这些模块时获取的代码与首次下载完全一致,从而避免项目依赖的模块因恶意、意外或其他原因发生意外变更。 go.modgo.sum 都应纳入版本控制。

5. 升级依赖

Go Modules 使用语义化版本:

vMAJOR.MINOR.PATCH

例如:

v0.1.2
major = 0
minor = 1
patch = 2

升级到最新版本:

go get golang.org/x/text

等价于:

go get golang.org/x/text@latest

指定版本:

go get rsc.io/sampler@v1.3.1

查看某个 module 可用版本:

go list -m -versions rsc.io/sampler

重要点:

  • go get 用来下载或改变依赖版本。
  • 默认是 @latest。
  • 但 latest 不一定适合你的项目,升级后必须跑回归测试。

6. indirect 依赖

go.mod 中可能出现:

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

// indirect 表示:当前 module 没有直接 import 它,它是被其他依赖间接引入的。

也就是:

  • 你的代码直接依赖 A
  • A 又依赖 B
    那么 B 可能会以 indirect 的形式出现在 go.mod 中

7. 主版本(Major Version)升级:v2+ 必须进入 import path

这是 Go Modules 最关键的设计之一。

v2 开始,module path 必须带主版本号:

import "rsc.io/quote/v3"

而不是:

import "rsc.io/quote"

例如:

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

这意味着:

它们是不同的 module path,可以同时存在。

这个设计叫:Semantic Import Versioning

核心意义是:不兼容的大版本,用不同 import path 表示。

好处是:一个项目可以渐进迁移。旧代码继续用 v1;新代码可以先引入 v3;等准备好后再逐步替换。

为什么只针对 Major Version?
因为不同 Major Version 可能出现对用户提供的 Api 的不兼容修改。而小版本的演进通常保证用户 Api 兼容.

8. 同一个 module path 只能有一个版本

Go 构建时允许:

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

因为它们的 module path 不同。

但不允许同一次构建里同时存在:

rsc.io/quote v1.5.2
rsc.io/quote v1.6.0

因为它们是同一个 module path。

核心规则:

  • 同一个 module path,构建中最多只能选一个版本。
  • 不同 major version,因为路径不同,所以可以并存。

9. 迁移到新 major version

假设原来:

import "rsc.io/quote"

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

迁移到 v3:

import "rsc.io/quote/v3"

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

大版本升级意味着 API 可能变了。

所以迁移步骤通常是:

  1. 引入新 major version
  2. 查看新版本文档
  3. 修改 import path
  4. 修改 API 调用
  5. 跑测试
  6. 清理旧依赖

10. 删除无用依赖

即使代码已经不再使用旧依赖,它可能仍然留在 go.mod 中。

原因是:

go build / go test 可以发现“缺少依赖”,但不能安全判断“哪些依赖可以删除”。
因为 go build / go test 只加载当前命令需要构建的 packages,因此能发现当前构建缺少依赖;
但它不会穷举整个 module 的所有 package、测试依赖、平台文件和 build tags,所以不能安全删除 go.mod 中看似无用的依赖。

清理命令:

go mod tidy

它会:

  • 添加缺失依赖
  • 删除无用依赖
  • 整理 go.mod / go.sum

常用流程:

go mod tidy
go test ./...

11. 常用命令总结

命令 作用
go mod init <module-path> 初始化 module,生成 go.mod
go test / go build 构建/测试,并在需要时自动添加缺失依赖
go list -m all 查看当前 module 和完整依赖列表
go list -m -versions <module> 查看某个 module 的可用版本
go get <module> 添加或升级依赖到 latest
go get <module>@<version> 切换依赖到指定版本
go mod tidy 清理无用依赖,补齐缺失依赖

评论