Skip to content

Go 模块管理:弄清这次构建到底用了哪份代码 ​

同事改了一个依赖,你的程序为什么也变了?本地联调通过,CI 为什么找不到包?这些问题通常要同时看三件事:import 指向哪个模块、依赖图选中了哪个版本、主模块或工作区有没有把代码替换掉。

本文沿着一次构建把它们连起来。示例固定 Go 1.27.2、Linux/amd64;公开依赖固定 golang.org/x/text v0.28.0,其余实验用本地小模块,不需要私有仓库账号。实际输出和完整目录都在文中,私有仓库访问只解释规则,不冒充已经登录验证。

先分清四个名字 ​

名称它回答的问题例子
包 package哪组同目录源码一起编译、以什么路径导入?golang.org/x/text/unicode/norm
模块 module哪些包一起发布和版本化?golang.org/x/text
模块版本这次取哪份发布内容?v0.28.0
主模块 main module当前命令以谁的配置组织构建?示例自己的 go.mod 所在模块;工作区可有多个

go.mod 声明模块路径,包的导入路径通常是“模块路径 + 包子目录”。因此不必每建一个包就执行一次 go mod init。多放一个 go.mod 会新建模块边界,同时带来独立的版本和依赖维护责任。模块与包

本文的建议是先用一个模块。只有代码确实需要独立发布、独立依赖约束,或者正在联调多个已有模块,才把模块拆开。

go.mod 选依赖,go.sum 留校验依据 ​

先看一个真正用到外部模块的调用:

go
input := "e\u0301"
output := norm.NFC.String(input)
fmt.Printf("before=%d after=%d equal=%v\n", len(input), len(output), output == "é")
text
before=3 after=2 equal=true

这里调用固定版本的 Unicode NFC 规范化 API,将 e 和组合重音规范化为合成字符;字节长度变化来自此输入的 UTF-8 编码,不能推广成“规范化一定缩短字符串”。norm v0.28.0

对应 go.mod 只需明确模块路径、Go 版本和依赖:

go.mod
module example.com/v2/moduletext

go 1.27.2

require golang.org/x/text v0.28.0

go 行是模块要求的最低 Go 版本,并影响相应语言版本规则;它与“我的电脑碰巧装了哪个编译器”不同。toolchain 行可以为主模块建议工具链,而 GOTOOLCHAIN=local 会阻止自动下载和切换。本次实验固定本地工具链,避免验证时版本悄悄改变。go 指令、工具链选择

第一次获取固定依赖后,go.sum 中能看到两类记录:

text
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=

前者校验模块内容,后者校验该版本的 go.mod。go.sum 不是“只准用这些版本”的完整锁文件:它可保留多个版本或只读过元数据的版本,真正的版本选择仍由依赖图决定。建议把两份文件一同提交,审查依赖变化时同时看它们。内容校验

要做的事常用命令要看什么
初始化模块go mod init example.com/app模块路径是否稳定、能否对应发布位置
升级到明确版本go get example.com/lib@v1.4.2不仅直接依赖,传递依赖是否也变了
同步 imports 与依赖声明go mod tidy新增和移除的依赖是否符合预期
看最终版本go list -m all版本、替换路径;vendor 模式需显式 -mod=mod
看谁依赖谁go mod graph每条边要求的版本,不等于每条边各装一份
查引入原因go mod why -m 模块路径从主模块包到该依赖的路径
校验缓存go mod verify缓存中的模块是否被修改,不是漏洞扫描

这些命令的职责来自 go mod 命令参考。本文实测先下载并校验,普通运行与 vendor 运行均输出同一行规范化结果;go mod verify 输出 all modules verified。这只证明本次缓存一致,不能证明依赖没有恶意逻辑或已知漏洞。

“最小版本选择”为什么选出了较高版本 ​

假设主模块直接要求 shared v1.1.0,alpha 要求 v1.2.0,beta 要求 v1.4.0。所有路径都叫 example.com/shared,一次构建不会给不同调用方分别塞一份不同版本。

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    App["主模块"] -->|"require v1.0.0"| A["alpha"]
    App -->|"require v1.0.0"| B["beta"]
    App -->|"至少 v1.1.0"| S["shared:选中 v1.4.0"]
    A -->|"至少 v1.2.0"| S
    B -->|"至少 v1.4.0"| S
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    App["主模块"] -->|"require v1.0.0"| A["alpha"]
    App -->|"require v1.0.0"| B["beta"]
    App -->|"至少 v1.1.0"| S["shared:选中 v1.4.0"]
    A -->|"至少 v1.2.0"| S
    B -->|"至少 v1.4.0"| S

MVS(Minimal Version Selection)在依赖图提出的最低要求中,为同一模块路径选取最高的要求版本;它不会为了“新”而自动去选仓库里最新的 v1.9.0。这里的“最小”是满足当前图的要求,不是选择数字最小的 v1.1.0。MVS 规则、原始设计

附录创建三份本地依赖及主模块,用 replace 提供内容,再执行:

bash
go mod edit -require=example.com/shared@v1.1.0
go list -mod=mod -m all

实际结果:

text
example.com/mvsapp
example.com/alpha v1.0.0 => ../alpha
example.com/beta v1.0.0 => ../beta
example.com/shared v1.4.0 => ../shared

-mod=mod 允许命令更新主模块中的依赖记录;本例随后 go mod graph 里主模块的 shared 边也已变成 v1.4.0。只查看而不允许改动时,保持文件已同步并用默认只读行为,遇到“updates to go.mod needed”先检查差异,不要把它理解为下载失败。

版本号表达的是兼容承诺 ​

变化常见版本变化对调用方的含义
兼容修复v1.2.3 → v1.2.4API 兼容,仍需回归测试
兼容新增功能v1.2.3 → v1.3.0旧调用应继续成立
不兼容公开 API 变化v1 → v2模块通常改成 example.com/lib/v2,imports 也改
未稳定发布v0.x 或预发布后缀不应假设与此前版本兼容
未打 tag 的提交pseudo-version由工具将提交映射成可排序版本,不手工拼时间戳

这是语义化版本的约定和 Go 的主版本路径规则,不是编译器替发布者证明兼容。/v2 让新旧大版本成为不同模块路径,因此可在同一构建中共存;gopkg.in 等有专门的路径规则,不能把 /v2 口诀无条件套到所有历史仓库。SemVer 2.0.0、主版本后缀与例外

replace 和 go work:方便联调,也会隐藏缺失的发布依赖 ​

replace example.com/shared => ../shared 只改变代码来源,不会自动把这个模块加进依赖图,还需要 require 或其他依赖路径。依赖模块自己写的 replace 不会传染给上游调用者;有效的是主模块及工作区的替换规则。换句话说,别人不能靠发布一份 go.mod 悄悄替你重定向依赖。replace

联调办法适合情形容易遗漏的事
主模块写本地 replace只想在一个应用里临时替换一份依赖本机相对目录在 CI 不存在;发布前清理或正式版本化
go.work 的 use多个模块要同时作为主模块开发其他模块的本地内容可能掩盖依赖声明错误
go.work 的 replace工作区统一替换依赖工作区级规则影响参与构建的模块,要审清范围

在本例中,app 声明依赖 greeting v0.1.0,却没有下载这个发布版本。go.work 同时 use app 和 greeting,于是运行 app 得到:

text
local greeting

同一目录切换到 GOWORK=off GOPROXY=off go run .,实际失败:

text
main.go:4:2: missing go.sum entry for module providing package example.com/greeting ...

错误省略号只省去了重复模块路径,原始输出保留在配套日志中。这个失败表明 app 尚不能凭自身声明在本例离线环境独立构建,不是说明 greeting 的远端一定不存在。工作区运行成功也不等于已发布的依赖可用。工作区规则

建议在发布模块的 CI 中再做一遍 GOWORK=off go test ./...,检查独立构建。go.work 是否入库要按仓库用途决定:统一开发的多模块仓库可以共享工作区;供外部使用的模块更应防止 CI 意外依赖开发者本地配置。go env GOWORK 能先回答“当前究竟启用了哪份工作区”。工作区与 CI 的官方提醒

私有模块:先决定请求该发给谁 ​

GOPROXY 控制从代理还是版本控制系统取模块。逗号分隔时,前一个代理通常只有返回 404/410 才继续回退;竖线分隔允许在任意错误后继续。把任意错误都当成“再问公共服务”,可能让原本只应留在公司内部的模块路径外泄。代理回退规则

GOPRIVATE 用模块路径前缀的 glob 模式说明哪些模块是私有的;默认同时影响“不经公共代理”的 GONOPROXY 与“不查公共校验数据库”的 GONOSUMDB。它不提供 Git 账号、证书或访问授权。私有模块

bash
# 说明性配置:example.corp 为占位域名,本文没有访问它。
GOPRIVATE='example.corp/*' go mod download

若公司代理同时提供私有和公共依赖,可能要专门配置 GONOPROXY 让私有模块仍走该代理。这应按公司网络和凭据政策设置,不能从一个博客复制到所有环境。排错时可先查看 go env GOPROXY GOPRIVATE GONOPROXY GONOSUMDB;不要把含凭据的代理 URL 或诊断输出直接贴进公开工单。

公共模块的 checksum database 帮助核对同一模块版本的内容一致性;它并不审计依赖的行为。为了省一次配置就永久关闭 GOSUMDB,会损失一层完整性检查。本文对真实公开依赖显式使用官方代理与校验服务,纯本地示例则可离线完成。校验数据库

vendor:把构建所需依赖带进仓库 ​

go mod vendor 将构建和测试主模块所需的依赖包复制到 vendor,并生成 vendor/modules.txt。它不是把整个模块缓存完整备份;依赖自己的测试包等内容不会一概保留。go 版本和目录满足条件时会自动使用 vendor,本例用 go run -mod=vendor . 把选择写明。vendor 命令及一致性规则

收益是依赖源码可随仓库审查,构建可少依赖下载链路;代价是仓库体积和同步成本。不要手改 vendor 后以为 go.sum 会自动认证这些改动,也不要把“有 vendor”当成所有工具、构建脚本和系统依赖都离线可用。

本例重复运行时遇到过一个直接体现规则的错误:存在 vendor 后,go list -m all 不能从 vendor 算出完整模块图。改成 go list -mod=mod -m all 即可查看模块图;真正要验证 vendor 构建时仍用 -mod=vendor。这是显式选择不同工作模式,不需要删除整个模块缓存。

资料与验证边界 ​

2026-10-13,Go 1.27.2、Linux/amd64 实际执行公开模块下载与校验、普通及 vendor 运行、工作区成功/禁用工作区失败、MVS 选版。没有执行私有仓库访问、线上发布或企业代理配置。目录中的 MVS 模块是教学用本地内容,replace 不证明对应远端 tag 存在。

完整复现实验 ​

将下面文件按路径保存,工作目录设为包含 run_modules.sh 的目录。使用 Go 1.27.2,设置 GOTOOLCHAIN=local 和 GOFLAGS=-buildvcs=false;执行 bash run_modules.sh。脚本中的公开下载需要网络;工作区失败用例是预期结果,脚本会检查它确实失败。vendor 是脚本生成物,无需手工复制其源码。

run_modules.sh ​

bash
#!/usr/bin/env bash
set -eu
cd "$(dirname "$0")/modules"
printf '\n== pinned public module ==\n'
cd text
GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org go mod download all
GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org go mod verify
go run .
go list -mod=mod -m all
go mod vendor
go run -mod=vendor .
cd ../work/app
printf '\n== workspace ==\n'
go env GOWORK
go run .
printf '\n== same app without workspace: expected failure ==\n'
if GOWORK=off GOPROXY=off go run .; then exit 1; else echo "expected_failure=true"; fi
cd ../../mvs/app
printf '\n== MVS selected shared version ==\n'
go mod edit -require=example.com/shared@v1.1.0
go list -mod=mod -m all
go mod graph

modules/mvs/alpha/go.mod ​

go.mod
module example.com/alpha

go 1.27.2

require example.com/shared v1.2.0

modules/mvs/app/go.mod ​

go.mod
module example.com/mvsapp

go 1.27.2

require (
	example.com/alpha v1.0.0
	example.com/beta v1.0.0
	example.com/shared v1.4.0
)

replace example.com/alpha => ../alpha

replace example.com/beta => ../beta

replace example.com/shared => ../shared

modules/mvs/beta/go.mod ​

go.mod
module example.com/beta

go 1.27.2

require example.com/shared v1.4.0

modules/mvs/shared/go.mod ​

go.mod
module example.com/shared

go 1.27.2

modules/text/go.mod ​

go.mod
module example.com/v2/moduletext

go 1.27.2

require golang.org/x/text v0.28.0

modules/text/go.sum ​

text
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=

modules/text/main.go ​

go
package main

import (
	"fmt"
	"golang.org/x/text/unicode/norm"
)

func main() {
	input := "e\u0301"
	output := norm.NFC.String(input)
	fmt.Printf("before=%d after=%d equal=%v\n", len(input), len(output), output == "é")
}

modules/work/app/go.mod ​

go.mod
module example.com/workapp

go 1.27.2

require example.com/greeting v0.1.0

modules/work/app/main.go ​

go
package main

import (
	"example.com/greeting"
	"fmt"
)

func main() { fmt.Println(greeting.Message()) }

modules/work/go.work ​

text
go 1.27.2

use (
 ./app
 ./greeting
)

modules/work/greeting/go.mod ​

go.mod
module example.com/greeting

go 1.27.2

modules/work/greeting/greeting.go ​

go
package greeting

func Message() string { return "local greeting" }

保留各书来源与许可 · 示例运行条件以正文为准