Monorepo 的困境
你的团队在使用 monorepo。所有 Go 服务都放在一个 Git 仓库里:
monorepo/
├── go/
│ ├── auth/
│ │ └── go.mod # module github.com/org/monorepo/go/auth
│ ├── config/
│ │ └── go.mod # module github.com/org/monorepo/go/config
│ └── logger/
│ └── go.mod # module github.com/org/monorepo/go/logger
├── proto/
├── deploy/
└── README.md
三个 Go 模块,一个仓库。这是常见的模式——共享 CI、原子化的跨模块修改、一致的版本管理。
但 import path 又长又丑,还嵌套得很深:
import (
"github.com/org/monorepo/go/auth"
"github.com/org/monorepo/go/config"
"github.com/org/monorepo/go/logger"
)
而且绑定在 GitHub 上。要迁移到 GitLab?所有 import 都得改。
你想要 vanity URL:
import (
"go.company.com/auth"
"go.company.com/config"
"go.company.com/logger"
)
但问题来了:传统的 go-import meta 标签是一个 import path 映射一个 repo。你的 monorepo 是一个 repo 里有三个模块。怎么搞?
Go 1.25 的解法:子目录模块
Go 1.25 引入了子目录模块支持(subdirectory module)。go-import meta 标签现在接受可选的第 4 个字段——仓库内 go.mod 文件所在的子目录路径:
<meta name="go-import" content="go.company.com/auth git https://github.com/org/monorepo go/auth">
这告诉 Go 工具链:“模块 go.company.com/auth 位于 https://github.com/org/monorepo 的 go/auth 子目录中。”
当你运行 go get go.company.com/auth 时:
- Go 访问
https://go.company.com/auth?go-get=1 - 找到上面的 meta 标签
- 克隆
https://github.com/org/monorepo - 在
go/auth/下查找go.mod - 从该子目录拉取模块
一个仓库,多个模块,简洁的导入路径。正是 monorepo 团队需要的。
用 gvu 配置
gvu CLI 通过 --subdir 参数支持子目录模块。以下是完整流程。
第一步:安装 gvu
curl -fsSL gomodvanityurls.com/install.sh | bash
第二步:用 –subdir 添加路由
为 monorepo 中的每个模块添加路由,指定子目录:
# auth 模块在 go/auth/ 子目录
gvu add go.company.com/auth https://github.com/org/monorepo.git --subdir go/auth
# config 模块在 go/config/
gvu add go.company.com/config https://github.com/org/monorepo.git --subdir go/config
# logger 模块在 go/logger/
gvu add go.company.com/logger https://github.com/org/monorepo.git --subdir go/logger
每条命令会创建一个带子目录映射的 vanity URL 路由。服务器会自动生成正确的 4 字段 go-import meta 标签。
第三步:更新每个模块的 go.mod
每个模块的 go.mod 必须将 vanity URL 声明为模块路径:
// go/auth/go.mod
module go.company.com/auth
go 1.25
// go/config/go.mod
module go.company.com/config
go 1.25
// go/logger/go.mod
module go.company.com/logger
go 1.25
第四步:配置 GOPRIVATE
go env -w GOPRIVATE=go.company.com
第五步:验证
$ go get go.company.com/auth
go: downloading go.company.com/auth v0.1.0
go: added go.company.com/auth v0.1.0
三个模块现在都可以用简洁的 vanity path 来导入了。
规模化:子目录通配(W2)
三个模块还好手动添加。如果你的 monorepo 有 20 个模块呢?
monorepo/
├── go/
│ ├── auth/go.mod
│ ├── config/go.mod
│ ├── logger/go.mod
│ ├── cache/go.mod
│ ├── queue/go.mod
│ ├── db/go.mod
│ ├── http/go.mod
│ ├── grpc/go.mod
│ ├── metrics/go.mod
│ ├── tracing/go.mod
│ ├── ...
逐个添加 20 条路由太麻烦了。gvu 的子目录通配(W2)用一条规则搞定:
gvu add go.company.com/libs https://github.com/org/monorepo.git '--subdir=*'
这一条规则自动匹配任何子目录:
| 导入路径 | 解析结果 |
|---|---|
go.company.com/libs/auth | github.com/org/monorepo 子目录 go/auth |
go.company.com/libs/config | github.com/org/monorepo 子目录 go/config |
go.company.com/libs/logger | github.com/org/monorepo 子目录 go/logger |
往 monorepo 里加新模块?自动可导入——不需要改任何路由。
注意:子目录通配需要 Pro 账户和自定义域名。详见通配路由。
Monorepo 最佳实践
1. 统一子目录规范
选一种结构,所有 monorepo 都遵守:
# 方案 A:go/<module>/
monorepo/go/auth/go.mod
monorepo/go/config/go.mod
# 方案 B:modules/<module>/
monorepo/modules/auth/go.mod
monorepo/modules/config/go.mod
# 方案 C:services/<module>/
monorepo/services/auth/go.mod
一致性让 --subdir 模式可预测,通配规则更简洁。
2. 按模块打 tag
monorepo 中的 Go 模块需要模块级别的 tag:
# 给 auth 打 v1.0.0
git tag go/auth/v1.0.0
# 给 config 打 v0.3.0
git tag go/config/v0.3.0
tag 前缀必须匹配子目录路径。Go 用这些 tag 来确定模块版本。
3. 保持 go.mod 精简
每个子模块的 go.mod 只声明实际使用的依赖。避免不必要的兄弟模块导入——保持依赖图干净。
4. 本地开发用 replace 指令
在同一个 monorepo 中跨模块开发时,使用 replace 指令:
// go/auth/go.mod
module go.company.com/auth
go 1.25
require go.company.com/config v0.3.0
replace go.company.com/config => ../config
这样可以在本地测试改动,不需要先推 tag。
5. 跨模块修改,一个 commit 搞定
monorepo 最大的优势:原子化的跨模块修改。
# 修改 config 的 API,同时更新 auth 使用新 API,一个 commit 搞定
git add go/config/ go/auth/
git commit -m "feat(config): add Validate(); update auth to use it"
git tag go/config/v0.4.0
git tag go/auth/v1.1.0
git push --tags
不需要版本协调。不需要"先发 config,再更新 auth"的流程。
前后对比
| 维度 | 之前(裸 GitHub 路径) | 之后(vanity URL + subdir) |
|---|---|---|
| 导入路径 | github.com/org/monorepo/go/auth | go.company.com/auth |
| 仓库迁移 | 重写所有 import path | 更新路由目标 URL |
| 新增模块 | 加到 repo + 更新所有消费者 | 加到 repo + 一条 gvu add |
| 品牌形象 | 像是子目录 | 像是正式产品 |
| IDE 支持 | 深层嵌套路径 | 简短清晰的路径 |
总结
Go 1.25 的子目录模块支持 + gvu 的 --subdir 参数,是用 vanity URL 管理多模块 monorepo 最干净的方式:
- 一个仓库,多个独立版本的 Go 模块
- 简洁的导入路径,不暴露仓库结构
- 平台无关——换 Git 托管商不用改 import
- 子目录通配——一条规则覆盖所有现有和未来的模块
# 单模块
gvu add go.company.com/auth https://github.com/org/monorepo.git --subdir go/auth
# 所有模块一次搞定(Pro)
gvu add go.company.com/libs https://github.com/org/monorepo.git '--subdir=*'
试试 gvu——免费 5 条路由。你的 monorepo 值得拥有干净的导入路径。