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/monorepogo/auth 子目录中。”

当你运行 go get go.company.com/auth 时:

  1. Go 访问 https://go.company.com/auth?go-get=1
  2. 找到上面的 meta 标签
  3. 克隆 https://github.com/org/monorepo
  4. go/auth/ 下查找 go.mod
  5. 从该子目录拉取模块

一个仓库,多个模块,简洁的导入路径。正是 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/authgithub.com/org/monorepo 子目录 go/auth
go.company.com/libs/configgithub.com/org/monorepo 子目录 go/config
go.company.com/libs/loggergithub.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/authgo.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 值得拥有干净的导入路径。