一场本不该发生的迁移

我认识一个团队,花了三周时间把 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 稳定性是工程纪律,不是锦上添花。规则很简单:

  1. 拥有你的 import path——使用你控制的域名
  2. 不暴露基础设施——不要 IP,不要内部域名
  3. 不嵌入 Git 平台——身份和位置解耦
  4. 路径要浅——不要反映组织架构
  5. 用 tag 管理版本——不是 repo 名字
  6. 定期验证——测试 vanity URL 解析

遵守这些规则,你下一次 Git 平台迁移只要 5 分钟,而不是 5 周。

gvu 设置稳定的 import path——每个模块一条命令,或每个 org 一条通配规则。