← [ BACK_TO_NOTES ]

Go Modules

Go Modules 的模块边界、go.mod/go.sum、语义化版本、MVS、require/replace/exclude/retract/toolchain 与可复现构建。

#concept #growing #dev #go

[!info] related notes

Go Modules

这个概念为什么会出现

在 Modules 之前,Go 依赖 GOPATH 与各种 vendor 工具,版本语义弱、可复现性差、“左边 builds 右边挂”常见。

需要一套官方机制同时回答:

  1. 这个项目是谁(模块路径)
  2. 依赖谁、最低哪个版本
  3. 构建时到底选哪一版(确定性算法)
  4. 下载内容是否被篡改(校验和)
  5. 需要什么语言/工具链版本

Go Modules(Go 1.11 引入,Go 1.16 起模块感知模式默认开启)用 go.mod / go.sum + 最小版本选择(MVS) 解决上述问题。包(package)仍是编译单元;模块(module)是版本化的包集合。

[!abstract] 一句话理解 模块用 go.mod 声明路径与依赖要求;构建用 MVS 为每个模块选出满足所有要求的最高最低版本;go.sum 记录内容校验信息,不是选版锁文件;可复现还取决于工具链、构建参数与其他输入。

最小可运行示例

先把示例放进业务场景,再看代码:

场景:创建模块并调用 quote 库

在新目录创建 example.com/hello。保存下方 main.go 后,再执行 go get 与 tidy;如果只有空模块没有 import,tidy 会移除刚添加但未使用的依赖。本例只输出问候语,不启动服务。

# 1. 建目录并声明模块路径(导入路径前缀)
mkdir hello && cd hello
go mod init example.com/hello

# 2. 写入 main.go(业务:启动时打印一句外部库提供的 slogan)
# package main
# import (
#   "fmt"
#   "rsc.io/quote"
# )
# func main() { fmt.Println(quote.Hello()) }

# 3. 按版本拉取依赖,并整理 go.mod / go.sum
go get rsc.io/quote@v1.5.2
go mod tidy

# 4. 以当前模块为根运行
go run .

go.mod 的关键字段示意如下;go mod init 写入的 go 版本取决于所用工具链,tidy 通常还会写入间接依赖,因此这不是命令输出的完整快照:

module example.com/hello

go 1.22

require rsc.io/quote v1.5.2

对应 main.go 核心:

package main

import (
	"fmt"

	// 导入路径必须以模块路径为前缀(此处是第三方模块路径)。
	"rsc.io/quote"
)

// main:输出依赖库提供的问候语。
// 运行只能证明本次依赖解析与执行成功,不单独证明可复现。
// 教学点:import 的是“包路径”,版本约束写在 go.mod,不写在源码里。
func main() {
	fmt.Println(quote.Hello())
}

建议运行(在 hello 目录):

go run .

rsc.io/quote v1.5.2 的 Hello 会按环境语言选择问候语;英语环境中预期如下。POSIX shell 可用 LC_ALL=en_US.UTF-8 go run . 固定本例的语言输入:

Hello, world.

结合场景再看三个关注点

  1. module 行 = 身份与导入前缀
    本仓库自己的包都挂在 example.com/hello/... 下。

  2. go 行是工具/语言基线
    声明团队最低 Go 版本,影响可用语法与工具默认行为。

  3. go mod tidy 对齐真实 import
    多出来的 require 会删,缺的会补,并维护 go.sum 校验和。

核心概念与准确模型

模块 vs 包

模块 module包 package
边界从 go.mod 所在目录向下,到嵌套模块边界为止通常一个目录
版本语义化版本标签随模块版本
导入路径以 module path 为前缀import 路径指向包目录

example.com/hello/user 可以是模块 example.com/hello 下的包。

go.mod 主要指令

module

以下是两个不同主版本的替代写法,一个 go.mod 只能有一个 module 指令:

module example.com/hello
module example.com/hello/v2 // 常规模块的 v2+ 路径规则,特殊兼容规则见下文
  • 唯一标识模块。
  • 与版本一起构成模块版本。
  • v2+ 主版本在路径中加 /v2(semantic import versioning)。

go

go 1.22
  • 模块假定的最低 Go 版本。
  • 影响语言特性可用性、工具默认行为;Go 1.21+ 与工具链选择强相关。
  • 主模块 go 版本应 ≥ 依赖所要求的版本(工具会强制相关约束)。

toolchain(Go 1.21+)

toolchain go1.22.5

这是主模块/工作区的建议工具链名,不是要求所有使用者精确采用此补丁版;依赖模块里的 toolchain 不直接约束调用方。go 行表达最低要求,GOTOOLCHAIN 策略决定是否允许自动切换/下载;GOTOOLCHAIN=local 禁止自动换用其他工具链。

require

require (
    example.com/other v1.2.3
    golang.org/x/sync v0.6.0 // indirect
)
  • 声明依赖的最低版本要求。
  • // indirect 表示非本模块源码直接 import,而是图中需要。
  • Go 1.17+ 更明确地列出传递依赖以支持惰性加载/图剪枝。

replace(主模块或工作区)

以下三条是不同替代方案,不要把同一路径的冲突规则同时写入同一 go.mod:

replace example.com/other => ../other
replace example.com/other v1.2.5 => example.com/other v1.2.3
replace example.com/other => example.com/myfork/other v1.2.3-fixed
  • 重定向已进入模块图的模块获取位置或版本;replace 本身不会添加 require。指向本地目录时,该目录必须是具有 go.mod 的模块。
  • 不改变 import 路径字符串。
  • 依赖你的模块的人不会自动继承你的 replace。
  • 适合本地联调、临时 fork;发布库应谨慎。

exclude(仅主模块)

exclude example.com/other v1.3.0

从图中排除特定不良版本。

retract

retract v1.1.0 // published by mistake
retract [v1.0.0, v1.0.5] // broken build

声明本模块的指定版本不适合新选择,应在可被工具发现的版本中发布 retract 信息。它不删除版本、不自动把现有 require 升级,也不阻止显式按该版本获取。

go.sum

  • 记录模块内容与 go.mod 的加密哈希。
  • 用于验证下载内容与已有校验信息一致;首次获取还依赖所用代理、校验数据库与信任配置,哈希不证明代码安全。
  • 不决定选择哪个版本,可能保留多个版本的哈希。文件存在且有依赖校验内容时应提交;纯标准库模块可能没有 go.sum。
  • 不要手改;由 go 命令维护。

语义化版本与伪版本

  • 标签:vMAJOR.MINOR.PATCH(可加 pre-release)。
  • 无合适发布标签的提交可使用伪版本,例如 v0.0.0-yyyymmddhhmmss-commithex;前缀和格式还取决于已有基准版本,应由 go 命令生成。
  • 常规模块的 v0/v1 不带主版本后缀,v2+ 使用 /vN。
  • 例外包括 gopkg.in 的 .vN 路径,以及尚未迁移到模块的旧仓库可能出现的 +incompatible 版本;不能机械给所有旧导入路径追加 /v2。
  • major 变更 = 新模块路径,可与旧 major 并存于同一构建。

最小版本选择(MVS)

构建模块图时:对每个模块路径,在所有 require 约束中取最高的那个最低版本。

性质:

  1. 确定性:同样输入图 → 同样选择。
  2. 不自动追新:只在给定模块图要求中选择版本;远端新增修复版本不会自动进入构建,需要显式升级依赖。
  3. 无 SAT 求解器:不尝试找复杂的最大/最优集合。
  4. 理解现象:“我没直接依赖 X v1.9,但被拉到 v1.9”——因某传递依赖要求 ≥v1.9。

常用命令

命令作用
go mod init创建模块
go get path@version添加/升级/降级依赖
go mod tidy按 import 整理 require 与 go.sum
go mod download下载模块到缓存
go mod vendor写出 vendor/
go mod graph打印依赖图
go list -m all列出选中模块版本
go list -m -u all查看可升级
go mod why pkg解释为何需要某包

模块缓存与代理

  • 默认经模块代理(如 proxy.golang.org)与校验数据库。
  • GOPROXY 配置代理,GOSUMDB 配置校验数据库;GOPRIVATE 匹配路径并为 GONOPROXY/GONOSUMDB 提供默认值。它不自动配置访问凭证,显式配置的其他变量也可能改变行为。
  • 私有库需配置 VCS 访问与 GOPRIVATE=*.corp.example.com。

Workspace(go.work)

go.work 的 use 将多个模块作为主模块共同参与构建,也可声明 replace;它可能改变相同目录中的选版和替换结果。验证单个发布模块时应额外检查 GOWORK=off 的独立构建,避免工作区掩盖缺失依赖。

设计动机

  1. 为可复现提供基础 要求 + 校验和 + 确定性选版;仍需固定工具链、生成步骤及环境等其他输入。
  2. 兼容性与 major 路径
    用 import path 表达不兼容 major,避免“同 path 不同 major 地狱”。
  3. 主模块特权明确
    replace/exclude 不传染,防止上游绑架下游依赖图。
  4. 与包模型解耦
    语言仍只看见包;版本是工具链层。

边界情况与反直觉行为

1. 升 major 必须改 import path

常规模块发布 v2.0.0 时应同步更改 module/import 路径;gopkg.in 和历史非模块仓库的兼容规则另见上文。

2. replace 只对主模块

库作者在 go.mod 写 replace 本地路径,CI 克隆后别人依赖该库时无效。

3. tidy 与未使用 require

tidy 检查主模块的包、测试及所需依赖,考虑不同平台和大多数 build tags(不含 ignore),不只是当前平台的 build。运行前需生成相关源码;工具依赖按目标 Go 版本采用受支持的声明方式。反射或外部配置提到一个包名,不会自动让 tidy 知道依赖。

4. 间接依赖升级

go get example.com/lib@latest 可能牵动一大片传递版本(MVS)。

5. go 行过旧/过新

过旧:无法使用新语法。过新:使用旧工具链的用户/CI 可能直接失败(1.21+ 更严格)。

6. 收缩(retract)不是删除

旧版本仍可能被已有用户获取;retract 影响新选择与 list 展示。

7. 厂商目录

vendor 存在时,构建可优先用 vendoring 模式(-mod=vendor);团队需统一策略。

常见误区

[!warning] 常见误区:把 go.mod 当 npm 的随意版本范围文件 错误:以为写了 v1.2.3 就“只锁定 1.2.3 永不升”。
正确:那是最低要求;图中更高要求会抬升;真正字节级内容靠 sum 与选中版本。

[!warning] 常见误区:不提交 go.sum 缺少已有校验信息会影响下载验证,某些只读构建会报告 missing go.sum entry;这与 MVS 选版是不同机制。 正确:提交 go.mod 与需要的 go.sum;不要给不需要它的纯标准库模块制造空锁文件。

[!warning] 常见误区:库发布依赖 replace 错误:指望下游继承 fork replace。
正确:合并修复发布真版本,或让下游自己 replace。

[!warning] 常见误区:v2 不改路径 错误:破坏语义化导入版本规则。
正确:module example.com/m/v2 且 import 带 /v2。

[!warning] 常见误区:手工狂改 go.sum 错误:校验失败难查。
正确:go mod tidy / go get。

与相邻概念对比

概念差异
包与可见性源码边界;modules 是分发与版本边界
GOPATH 时代无一等版本;已被 modules 取代
Docker 镜像固定固定构建/运行环境的另一层;模块图与校验和不能覆盖所有环境输入
语义化版本(通用)Go 额外用 major 路径表达 breaking

工程实践

  1. go mod init 用真实可导入路径(与仓库/域名策略一致)。
  2. CI:go test ./... 前 go mod download 或依赖缓存;校验 go.sum。
  3. 常规:go get 升级 → 测试 → commit mod/sum。
  4. 私有模块:GOPRIVATE + SSH/HTTPS 凭证。
  5. 应用可积极升级;库要保守并测兼容。
  6. 单仓多模块用 go.work 本地开发,发布仍以各 go.mod 为准。
  7. 不要把 secrets 放进模块;模块内容进缓存与代理。
  8. 文档写明最低 Go 版本(与 go 行一致)。
  9. 事故版本立刻 retract 并发修复版。
# 查看为何依赖某模块
go mod why -m github.com/some/dep

# 升级某依赖到最新兼容
go get example.com/other@latest
go mod tidy

可验证实验

实验 1:init + tidy

空模块写 import "fmt" 与第三方库,观察 require 与 go.sum 变化。

实验 2:MVS 抬升

构造主模块 require A v1.0.0,A require B v1.2.0,主模块再 require B v1.1.0,用 go list -m all 看 B 选中版本。

实验 3:replace 本地

在主模块 require 已使用的 foo 模块,再以 replace 指向含 go.mod 的本地目录;验证修改本地代码影响构建。仅添加 replace 而没有任何需要该模块的关系,不会把模块引入图中。

实验 4:private

临时设置错误 GOPROXY/GOPRIVATE,阅读失败信息。

实验 5:v2 路径

阅读任意 /v2 模块的 go.mod 与 import 写法。

本节总结

  • 模块 = 版本化包集合;go.mod 描述身份与要求;go.sum 校验。
  • MVS 确定性选版;require 是下限不是“精确钉死唯一可能”。
  • replace/exclude 不从普通依赖继承;工作区还可提供 replace。retract 声明不推荐选用的版本。
  • v2+ 改路径。
  • 下一步:项目布局 与 构建部署。

自测题

概念题

  1. 为什么说 require 的版本是“最低要求”?
  2. 为何库的 replace 不能指望下游自动使用?
  3. 主版本 v2 为何要在 module path 加 /v2?

代码/命令推理题

go list -m all 显示某间接依赖比你直接 require 的更新。可能原因?

工程思考题

CI 出现 missing go.sum entry。应如何修复?是否应在 CI 用 go mod tidy 自动改文件?

参考答案

展开
  1. MVS 会在所有约束中取每个模块的最高下限,故实际选中版本可高于你写的 require。
  2. replace 仅影响主模块构建,不进入下游的模块图语义。
  3. 语义化导入版本:incompatible major 必须是不同 import path,以便并存与显式升级。
    推理题:传递依赖要求了更高版本,MVS 抬升。
    工程题:在开发机运行 go mod tidy 或补齐导致缺失的 go get,提交 go.sum;CI 应校验而非偷偷改依赖文件(除非专门依赖更新流水线)。

延伸阅读与资料来源

资料类型支撑
Go Modules Reference官方参考完整语义
go.mod 文件参考文档指令说明
Using Go Modules博客系列入门实践
Minimal Version Selection设计阐述MVS 思想
Module release & versioning文档版本与 v2+
Managing dependencies文档日常命令

笔记元信息

  • 建议文件名:go-modules.md
  • 所属阶段:工具链
  • 学习顺序:包与可见性之后
  • 建议下一篇:项目布局 或 构建与部署
  • 本篇状态:已深化(MVS、go.mod 指令与工程可复现)
创建于 2026/6/20 更新于 2026/10/6
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本