多模块本地联调不一定要改 go.mod
很多团队会把通用库和业务服务放在不同仓库或不同模块里。比如你正在开发 user-api,它依赖本地的 kit 模块。以前常见做法是在 user-api/go.mod 里写 replace:
replace example.com/kit => ../kit
这能工作,但如果你不小心把个人本地路径提交了,其他同事可能无法构建。go work 提供了另一种方式:在本地创建工作区,把多个模块临时组合起来开发,而不必修改每个模块的 go.mod。
这篇文章讲 go work 的入门用法和边界,以及 2024 年最新的实践建议。它不是每个项目都必须用,但在多模块联调时非常舒服。
一个多模块目录
假设目录:
workspace/
├── kit/
│ ├── go.mod
│ └── logx/
│ └── logx.go
└── user-api/
├── go.mod
└── main.go
kit/go.mod:
module example.com/kit
go 1.22
user-api/go.mod:
module example.com/user-api
go 1.22
require example.com/kit v0.1.0
user-api/main.go 导入:
import "example.com/kit/logx"
如果没有工作区,Go 会按版本去找 example.com/kit v0.1.0。但你现在想使用本地 ../kit 的最新修改。
完整 go.work 语法参考
go.work 文件的完整语法如下:
go 1.22
// use 指令:声明工作区中包含的模块目录
use (
./kit
./user-api
./billing-service
)
// replace 指令:在工作区层面替换依赖
replace example.com/old-lib => ./forks/old-lib
// replace 一个远程依赖到本地路径
replace github.com/some/lib => ./local-forks/lib
// replace 一个依赖到特定版本(与 go.mod 中 replace 语法相同)
replace example.com/lib => example.com/lib/v2 v2.1.0
各指令的详细说明:
go <version>:指定工作区所需的 Go 版本,必须不低于所有模块中声明的版本use <path>:添加一个模块到工作区。路径相对于go.work所在目录replace <old> => <new>:在工作区层面覆盖依赖解析规则
go.work 文件还支持 toolchain 指令(Go 1.21+):
go 1.22
toolchain go1.22.5
use (
./kit
./user-api
)
创建 go.work
在 workspace/ 目录:
go work init ./kit ./user-api
生成 go.work:
go 1.22
use (
./kit
./user-api
)
现在进入 user-api 运行:
go run .
Go 会优先使用工作区里的本地 kit 模块。你修改 kit/logx 后,user-api 能立刻使用新代码,不需要发布版本,也不需要改 user-api/go.mod。
添加模块:
go work use ./another-module
同步工作区:
go work sync
go work sync 会将工作区的依赖要求同步到各个模块的 go.mod 中,确保各模块之间的依赖一致性。这是发布前的重要步骤。
go.work 要不要提交
这取决于团队约定。如果仓库本身就是一个 monorepo,包含多个模块,并且大家都应该用同一个工作区,提交 go.work 是合理的。
如果只是你个人本地同时开发两个独立仓库,go.work 更像本地开发辅助文件,不一定适合提交。否则别人拉到后,路径结构不一致就可能困惑。
一个简单判断:go.work 描述的是项目标准结构,还是你个人机器上的临时组合?前者可以提交,后者不要提交。
在 .gitignore 中处理:
# 个人本地工作区不提交
go.work
# 但 monorepo 的标准工作区可以提交
# !go.work
go work 和 replace 的区别
replace 写在模块自己的 go.mod 里,会影响所有使用这个模块构建的人,除非它没有被提交。go work 写在工作区文件里,更适合本地多模块联调。
两者的详细对比:
| 特性 | replace (go.mod) | use (go.work) |
|---|---|---|
| 影响范围 | 所有导入该模块的项目 | 仅当前工作区的命令 |
| 版本控制 | 必须跟踪 | 可选择 |
| 适用场景 | 必要的依赖替换 | 临时本地联调 |
| 团队协作 | 影响所有人 | 仅影响本地 |
| 路径依赖 | 是 | 是 |
replace 仍然有用途。比如你要临时替换某个依赖到 fork 版本,并且这个替换就是项目当前需要的构建规则,可以写进 go.mod。但如果只是本地开发两个模块,go work 通常更干净。
不要同时在多个地方写复杂替换规则。依赖解析越绕,排查越难。团队应该明确当前推荐方式。
多版本 Go 模块联调
工作区中各模块需要使用兼容的 Go 版本。go.work 中的 go 指令指定了工作区的最小版本:
go 1.22
use (
./kit // go.mod 要求 go 1.21
./user-api // go.mod 要求 go 1.22
)
工作区的 go 版本(这里是 1.22)必须大于等于所有模块中的 go 版本。如果某个模块要求 go 1.23,而工作区声明 go 1.22,就会产生冲突。
解决版本冲突的方法:
- 将所有模块升级到兼容的 Go 版本
- 使用
toolchain指令指定更高的工具链版本 - 对于无法升级的模块,暂时不将其加入工作区
工作区与 vendor 的关系
vendor 和工作区是两种不同的依赖管理方式:
| 特性 | vendor | go work |
|---|---|---|
| 目的 | 锁定并离线存储依赖 | 本地多模块联调 |
| 版本控制 | 提交 vendor 目录 | 不提交模块代码 |
| 适用场景 | CI/CD 离线构建、依赖审查 | 本地开发联调 |
| Go 版本 | 所有版本 | Go 1.18+ |
可以同时使用两者:
# 在工作区中创建 vendor
go work vendor
这会为工作区下的所有模块创建统一的 vendor 目录。但通常不推荐,因为 vendor 和工作区的目标不同。
与 CI 集成策略
在 CI 中处理工作区的策略:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
# 1. 先在工作区模式下测试
- name: Test in workspace mode
run: go test ./...
# 2. 同步依赖到各模块
- name: Sync workspace
run: go work sync
# 3. 验证各模块独立构建(关闭工作区)
- name: Test without workspace
run: |
for dir in api sdk cli; do
cd $dir
GOWORK=off go test ./...
cd ..
done
GOWORK=off 测试是确保每个模块独立构建的最后一道防线。
go work sync 详解
go work sync 是 Go 1.21+ 引入的重要命令。它会:
- 读取工作区中所有模块的依赖
- 计算所有模块的依赖交集
- 更新各模块的
go.mod中的require指令 - 使各模块的依赖版本保持一致
# 在修改了多个模块后执行
go work sync
# 查看同步后的效果
git diff api/go.mod sdk/go.mod cli/go.mod
注意:go work sync 会修改 go.mod 文件。执行前应该确保工作区是干净的,或者先提交当前的改动。
工作区限制与注意事项
使用 go work 时需要了解以下限制:
- 路径限制:
use中的路径必须是go.work所在目录的子目录或同级目录 - 版本兼容性:工作区的
go版本必须 >= 所有模块的go版本 - 不能替代发布:工作区解决的是本地开发,发布仍然需要模块版本
- 编辑器兼容性:部分编辑器工具对工作区的支持可能不完善
- CI 一致性:CI 是否使用工作区应该与本地保持一致
常见问题排查:
# 检查当前工作区文件路径
go env GOWORK
# 查看模块解析结果
go list -m all
# 检查特定模块的来源
go list -m -json example.com/kit
replace 指令 vs 工作区对比
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 本地开发两个模块 | go work | 不污染 go.mod,退出工作区即恢复 |
| 临时 fork 一个依赖 | replace | 影响明确,可以提交 |
| 单模块项目 | 不需要 | 单模块不需要工作区 |
| monorepo 标准结构 | go.work(提交) | 团队统一使用 |
| 紧急补丁依赖 | replace | 需要持久化修复 |
Go 1.23+ 工作区改进
随着 Go 版本的演进,工作区功能不断完善:
- Go 1.18:工作区基础支持
- Go 1.19:更好的错误报告
- Go 1.20:
go work sync增强 - Go 1.21:
toolchain指令支持 - Go 1.22:依赖解析优化
- Go 1.23+:更智能的模块发现问题
建议始终使用最新的稳定 Go 版本进行开发,以获得最好的工作区体验。
实战:构建 API + SDK + CLI 的 monorepo
下面实战演示如何构建一个包含 API 服务、SDK 库和 CLI 工具的 monorepo。
目录结构:
mycompany/
├── go.work
├── api/ # HTTP API 服务
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ └── handlers/
├── sdk/ # 客户端 SDK
│ ├── go.mod
│ ├── go.sum
│ ├── client.go
│ └── models.go
└── cli/ # 命令行工具
├── go.mod
├── go.sum
└── main.go
api/go.mod:
module example.com/mycompany/api
go 1.22
require example.com/mycompany/sdk v0.1.0
sdk/go.mod:
module example.com/mycompany/sdk
go 1.22
cli/go.mod:
module example.com/mycompany/cli
go 1.22
require example.com/mycompany/sdk v0.1.0
go.work:
go 1.22
use (
./api
./sdk
./cli
)
开发流程:
# 1. 初始化工作区
cd mycompany/
go work init ./api ./sdk ./cli
# 2. 修改 sdk/client.go,添加新功能
# 3. 在 api 和 cli 中立刻使用新功能
go test ./api/... ./cli/...
# 4. SDK 测试通过后发布新版本
cd sdk/
git tag v0.2.0
git push origin v0.2.0
# 5. 同步到工作区
cd .. .
go work sync
# 6. 更新 api 和 cli 的 go.mod 依赖
cd api/ && go get example.com/mycompany/sdk@v0.2.0 && cd ..
cd cli/ && go get example.com/mycompany/sdk@v0.2.0 && cd ..
# 7. 验证独立构建
GOWORK=off go test ./api/... ./cli/...
一个真实联调流程
假设你发现 user-api 调用 kit/logx 时缺少一个字段。你可以先在 workspace 里创建工作区:
go work init ./kit ./user-api
然后修改 kit/logx:
package logx
func Fields(service string, requestID string) []interface{} {
return []interface{}{
"service", service,
"request_id", requestID,
}
}
在 user-api 里直接使用:
logger.Info("request finished", logx.Fields("user-api", requestID)...)
运行:
cd user-api
go test ./...
go run .
如果测试通过,你可以分别提交两个模块的改动。发布 kit 新版本后,再让 user-api 的 go.mod 升级到正式版本。这个流程比在 user-api/go.mod 里留下本地 replace 更干净。
需要注意的是,工作区只解决本地联调,不等于版本发布。CI 环境如果没有使用同一个 go.work,仍然会按 go.mod 里的版本解析依赖。因此合并前要确认依赖模块已经发布,或者 CI 也明确使用工作区结构。
常见问题:为什么我改了本地模块却没生效
遇到这种情况,先执行:
go env GOWORK
如果输出为空,说明当前命令没有处在工作区中。你可能不在 go.work 所在目录或其子目录下,也可能显式关闭了工作区。确认目录后再运行:
go list -m all
看依赖模块是否指向工作区版本。还要检查 go.work 里是否真的 use 了你修改的模块:
go work use ./kit
另一个常见原因是编辑器或终端打开了不同目录。Go 工具根据当前工作目录向上查找 go.work,目录不对时就会按普通模块模式工作。多模块项目里,保持终端、编辑器和测试命令的工作目录一致,会少很多困惑。
工作区中的版本管理与发布流程
使用工作区的多模块项目,需要清晰的版本管理流程。以下是一个推荐的工作流:
开发阶段
# 1. 拉取最新代码
git pull origin main
# 2. 创建工作区
go work init ./api ./sdk ./cli
# 3. 修改 sdk(新增功能)
# 编辑 sdk/client.go...
# 4. 在 api 中使用新功能
# 编辑 api/main.go...
# 5. 本地测试通过
go test ./...
发布阶段
# 1. 提交 sdk 改动
cd sdk/
git add .
git commit -m "feat: add new client method"
git push origin main
# 2. 发布 sdk 新版本
git tag v0.3.0
git push origin v0.3.0
# 3. 回到根目录,同步工作区
cd ..
go work sync
# 4. 更新 api 和 cli 的 go.mod
cd api/ && go get example.com/sdk@v0.3.0 && cd ..
cd cli/ && go get example.com/sdk@v0.3.0 && cd ..
# 5. 提交 api 和 cli 的依赖更新
git add api/go.mod api/go.sum cli/go.mod cli/go.sum
git commit -m "chore: update sdk to v0.3.0"
# 6. 验证独立构建
GOWORK=off go test ./...
紧急热修流程
发现生产 bug 需要紧急修复时:
# 1. 从生产标签创建修复分支
git checkout -b hotfix/patch v1.0.0
# 2. 修复 bug(在 sdk 中)
# 编辑 sdk/...
# 3. 发布 patch 版本 v1.0.1
git tag sdk/v1.0.1
git push origin sdk/v1.0.1
# 4. 更新依赖到 patch 版本
go get example.com/sdk@v1.0.1
# 5. 测试并发布
go test ./...
清晰的版本管理流程能避免很多"为什么线上和本地表现不一致"的问题。
go work 命令速查
# 初始化工作区
go work init [模块目录...]
# 添加模块到工作区
go work use [模块目录...]
# 移除模块(编辑 go.work 或)
go work edit -dropuse=[模块目录]
# 同步工作区依赖
go work sync
# 在工作区中创建 vendor
go work vendor
# 查看帮助
go help work
# 检查工作区状态
go env GOWORK
go list -m all
# 临时禁用工作区
GOWORK=off go test ./...
小结
go work 适合多个本地 Go 模块一起开发。它通过 go.work 文件声明工作区里的模块,让 Go 工具优先使用本地代码,减少临时 replace ../path 对 go.mod 的污染。
核心要点回顾:
go work init创建工作区,go work use添加模块go work sync同步依赖到各模块的go.modgo work是本地开发便利,go.mod是发布协议- 提交前用
GOWORK=off验证模块独立构建 - monorepo 可以提交
go.work,独立仓库间联调建议本地使用
它不是每个项目的必需品。单模块项目不需要;个人临时工作区不一定提交;monorepo 多模块项目可以把它作为标准开发方式。理解它解决的问题,比机械使用命令更重要。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。