一场本不该发生的迁移
我认识一个团队,花了三周时间把 120 个 Go 服务从一个 Git 平台迁移到另一个。代码搬迁只用了一天。剩下的两周半?在更新每一个 repo、每一条 CI 流水线、每一个 Dockerfile、每一个下游消费者的 import path。
最扎心的是:他们之前已经经历过一次了。两年前从 SVN 迁移到 Git,那时候也重写了所有 import path。
这种悲剧反复上演,因为大多数团队把 import path 当作偶然的——Git 平台给什么路径就用什么。但 import path 是身份标识。它是所有下游项目找到你的代码的方式。改了身份,就断了所有依赖你的人。
这篇文章讲的是如何让 import path 稳定——代码可以搬,基础设施可以换,import path 永远不用改。
Import Path 为什么会断
先盘点一下哪些情况会迫使你修改 import path:
| 触发事件 | 发生什么 | 影响范围 |
|---|---|---|
| Git 平台迁移 | GitHub → GitLab,或迁到自建平台 | 所有模块,所有消费者 |
| 组织重命名 | 公司改名,GitHub org 名称变更 | 所有模块,所有消费者 |
| 仓库转移 | 把 repo 移到另一个 org | 该模块 + 所有消费者 |
| 拆分 Monorepo | 大仓拆成独立仓库 | 拆出的模块 + 消费者 |
| 合并为 Monorepo | 多个独立仓库合入大仓 | 每个合入的模块 + 消费者 |
| 服务器搬迁 | 自建 Git 服务器 IP/域名变更 | 所有模块,所有消费者 |
发现规律了吗?每一个都是基础设施变更,不是代码变更。 你的代码没变,API 没变,但所有下游项目都要更新 go.mod、跑 go mod tidy、做一次完整的回归测试——只因为地址变了。
这与"稳定的身份标识"完全相反。
核心原则:代码搬迁,Import 不变
解法来自分布式系统的一个老思路:把身份和位置解耦。
在 Go 中,通过 vanity URL 实现——你控制的自定义 import path,指向代码实际所在的位置:
// import path 是你的。你说了算。
import "go.yourcompany.com/auth"
// 背后解析到代码当前所在的位置:
// 今天: github.com/yourcompany/auth
// 明年: gitlab.yourcompany.com/auth
// 后年: gitea.yourcompany.com/auth
Import path 永远不变。解析目标变了——那只是 vanity URL 服务上的一行配置更新,不是全公司的 import 重写。
迁移手册
当你确实需要迁移 Go 模块时(不管有没有 vanity URL),这是一份经过实战验证的检查清单。
第一阶段:迁移前
1. 盘点依赖图
动手之前,先搞清楚谁依赖了谁:
# 查找所有引用你模块的 repo
# (查看 Git 平台的 "used by" / "dependents" 页面,或搜索 go.mod 文件)
grep -r "github.com/yourorg/auth" --include="go.mod" .
了解影响范围。如果 50 个服务依赖模块 A,你就需要协调全部 50 个服务的更新。
2. 给当前版本打 tag
在迁移前给最后一个版本打 tag,让消费者可以锁定:
git tag v1.5.0 # 旧 import path 下的最后一个版本
git push --tags
3. 设置重定向(如果可行)
部分 Git 平台支持仓库重定向。GitHub 在改名后会从旧 URL 重定向到新 URL——但只是临时的,且仅限 git 操作。Go 模块代理(proxy.golang.org)可能缓存了旧路径,所以不要长期依赖重定向。
4. 配置 vanity URL(如果还没用)
如果你的 import path 绑死了 Git 平台,现在是解耦的时候:
# 迁移前设置 vanity URL
gvu add go.yourcompany.com/auth https://github.com/yourcompany/auth.git
# 更新模块的 go.mod 使用 vanity path
# (这是一次性的 breaking change——但也是你最后一次做这种事)
第二阶段:迁移中
5. 镜像,不是移动
过渡期间,保持新旧仓库同时可用:
# 完整克隆(含所有历史)
git clone --mirror https://github.com/yourcompany/auth.git
cd auth.git
# 推送到新平台
git push --mirror https://gitlab.yourcompany.com/yourcompany/auth.git
这会保留所有 tag、分支和历史。
6. 更新 vanity URL 目标
如果你在用 vanity URL,这是最简单的部分:
# 指向新平台——一条命令
gvu add go.yourcompany.com/auth https://gitlab.yourcompany.com/yourcompany/auth.git
不需要下游代码做任何修改。Import path go.yourcompany.com/auth 保持不变。
7. 如果没有 vanity URL:协调重写
没有 vanity URL 的情况下,每个消费者都需要更新:
# 在每个下游项目中:
# 方式 1:使用 replace 指令(临时方案)
go mod edit -replace github.com/yourcompany/auth=gitlab.yourcompany.com/yourcompany/auth
# 方式 2:全量替换 import path
find . -name "*.go" -exec sed -i 's|github.com/yourcompany/auth|gitlab.yourcompany.com/yourcompany/auth|g' {} +
go mod tidy
乘以消费者的数量。这就是那"几周工作量"的来源。
第三阶段:迁移后
8. 验证所有消费者可构建
# 每个下游项目
go build ./...
go test ./...
9. 更新 CI/CD
别忘了:
- CI 流水线配置(checkout 路径、Go 模块缓存)
- Dockerfile(如果引用了旧 import path)
- Docker Compose / Kubernetes 配置(如果镜像名来自 import path)
- 文档和 README
10. 清理旧仓库
所有消费者都迁移完成后:
- 归档(不要删除)旧仓库,至少保留 3 个月
- 加一个 README 指向新地址
- 保持旧 vanity URL 的重定向
稳定性检查清单
一旦你的 import path 稳定了,就让它保持下去。以下是该遵守的规则:
规则 1:拥有你的 import path
// ✅ 你控制这个域名
import "go.yourcompany.com/auth"
// ❌ 别人控制这个路径
import "github.com/yourcompany/auth"
如果 import path 里的域名不是你的,你就没有拥有这个路径。
规则 2:永远不要在 import path 里暴露基础设施
// ❌ IP 地址会变
import "192.168.1.50/auth"
// ❌ 内部域名会变
import "gitlab.internal.corp/auth"
// ✅ 域名稳定
import "go.yourcompany.com/auth"
规则 3:永远不要在 import path 里嵌入 Git 平台
// ❌ 绑死 GitHub
import "github.com/yourcompany/auth"
// ❌ 绑死 GitLab
import "gitlab.yourcompany.com/auth"
// ✅ 平台无关
import "go.yourcompany.com/auth"
规则 4:路径要浅、要有意义
// ✅ 短、有意义、稳定
import "go.yourcompany.com/auth"
import "go.yourcompany.com/db"
// ❌ 深层嵌套,反映组织架构(组织架构会变)
import "go.yourcompany.com/platform/team-alpha/auth-service"
组织架构调整是常态。你的 import path 不该体现哪个 VP 管哪个团队。
规则 5:用 tag 管理版本,不是用路径
// ✅ 语义化版本 tag
git tag v2.0.0
// ✅ 主版本后缀(Go 标准做法)
import "go.yourcompany.com/auth/v2"
// ❌ 把版本写在 repo 名字里
import "go.yourcompany.com/auth-v2"
规则 6:定期验证 vanity URL 解析
# 验证 meta 标签正确返回
curl -s 'https://go.yourcompany.com/auth?go-get=1' | grep go-import
# 预期输出:
# <meta name="go-import" content="go.yourcompany.com/auth git https://gitlab.yourcompany.com/yourcompany/auth">
# 验证 go get 正常
GOPROXY=direct go get go.yourcompany.com/auth@latest
Vanity URL 迁移:一分钟版
如果你已经在用 vanity URL,迁移就是这么简单:
# 迁移前:代码在 GitHub
gvu add go.yourcompany.com/auth https://github.com/yourcompany/auth.git
# ... 迁移发生 ...
# 迁移后:指向新平台。Import path 不变。
gvu add go.yourcompany.com/auth https://gitlab.yourcompany.com/yourcompany/auth.git
搞定。 所有下游的 go get go.yourcompany.com/auth 现在解析到新平台。任何消费者项目不需要改代码。
对于多模块的组织,一条通配规则覆盖全部:
# 一条规则覆盖 org 下所有仓库
gvu add 'go.yourcompany.com/*' 'https://github.com/yourcompany/*.git'
# 迁移?更新规则目标
gvu remove <route-id>
gvu add 'go.yourcompany.com/*' 'https://gitlab.yourcompany.com/yourcompany/*.git'
一条命令迁移 50 个模块。这就是"代码搬迁,Import 不变"的实际效果。
不稳定的代价 vs. 稳定的代价
| 方案 | 配置成本 | 迁移成本(每次) | 频率 |
|---|---|---|---|
| 裸 Git 平台路径 | 零 | 数周工程量 | 每次换平台 |
| Vanity URL | ~30 分钟 | 数分钟(更新路由目标) | 一次配置,永久使用 |
| Vanity URL + 通配 | ~5 分钟 | 数分钟(更新一条规则) | 一次配置,永久使用 |
Vanity URL 的配置成本相比一次迁移事件来说可以忽略不计。而且你只需要配置一次。
总结
Import path 稳定性是工程纪律,不是锦上添花。规则很简单:
- 拥有你的 import path——使用你控制的域名
- 不暴露基础设施——不要 IP,不要内部域名
- 不嵌入 Git 平台——身份和位置解耦
- 路径要浅——不要反映组织架构
- 用 tag 管理版本——不是 repo 名字
- 定期验证——测试 vanity URL 解析
遵守这些规则,你下一次 Git 平台迁移只要 5 分钟,而不是 5 周。
用 gvu 设置稳定的 import path——每个模块一条命令,或每个 org 一条通配规则。