API 版本管理
API 一旦上线就有了外部调用方,再想改字段含义或返回结构,就不再是"改个代码"那么简单。版本管理的本质是:用一套明确的规则,让你能安全演进接口,同时不强迫所有客户端同时升级。它的目标不是把每个小改动都包成一个新版本,而是只在真正破坏兼容性时才动用版本这个重武器。
为什么需要版本
判断一个改动是否需要版本,关键看它是不是破坏性变更(breaking change):
| 改动类型 | 是否破坏性 | 是否需要版本 |
|---|---|---|
| 新增可选字段 | 否 | 否 |
| 新增接口 | 否 | 否 |
| 删除字段 | 是 | 是 |
| 改字段含义 | 是 | 是 |
| 改字段类型(string → int) | 是 | 是 |
| 收紧校验规则 | 是 | 是 |
| 枚举值变动 | 看情况 | 通常需要 |
最容易踩的坑是"收紧校验"和"改字段含义"——它们不报错、不缺字段,但客户端按旧约定处理就会出错。这类隐性破坏比显式删字段更危险。
一条铁律:新增是免费的,删除和改义是昂贵的。能加字段就别改字段。
版本策略对比
| 策略 | 形式 | 优点 | 缺点 |
|---|---|---|---|
| URL 版本 | /v1/users、/v2/users | 显式、好调试、缓存友好 | 路径膨胀,多版本并存成本高 |
| Header 版本 | Accept: application/vnd.x.v2+json | URL 干净,可内容协商 | 不直观、难调试、CDN 易出错 |
| 查询参数 | ?version=2 | 改动小 | 易被忽略、缓存语义混乱 |
| 语义版本 | 整体 v2.3.1 | 表达力强 | 对 HTTP API 偏重,多用于 SDK/包 |
对大多数 HTTP API,URL 版本是最务实的选择:一眼能看出在调哪个版本,文档、Mock、监控都好对齐。Header 版本看似优雅,但调试时谁也记不住要带哪个 Accept,反而成了负担。
向后兼容的边界
不是所有变更都值得开新版本。可以安全地、不开版本地做的"兼容性扩展":
text
✓ 新增返回字段(客户端忽略未知字段即可)
✓ 新增可选请求参数
✓ 新增接口端点
✓ 放宽校验(原本拒绝的现在接受)
✗ 删除或重命名字段
✗ 改变字段类型
✗ 改变错误码语义
✗ 收紧校验(原本接受的现在拒绝)客户端能否"忽略未知字段",是兼容性的前提。如果客户端用强类型反序列化且严格校验,连新增字段都可能破坏它。上线前确认你的客户端约定。
兼容性的边界要提前和团队约定清楚:默认宽松读取、严格写入。读端宽容未知字段,写端允许额外字段被忽略。这样后续扩展才不会被迫开新版本。
废弃流程:不要直接删
破坏性变更的正确路径是"先标记废弃,再在新版本移除",而不是一刀切:
text
1. 在 v1 中字段继续返回,但加 deprecation 标记
2. 文档和响应头提示客户端迁移(Sunset / Deprecation header)
3. 观察旧字段的实际使用,给足迁移时间
4. 在 v2 中正式移除,v1 保持可用直到迁移完成http
Deprecation: true
Sunset: Wed, 31 Dec 2025 23:59:59 GMT
Link: </v2/users>; rel="successor-version"废弃周期长短取决于你有多少外部调用方、它们升级多快。内部 API 几周够了,开放平台可能要按年计。直接删字段等于对客户端发动一次线上故障,这是 API 设计里最该避免的操作。
同时维护多版本的成本
每多一个并存版本,维护成本就上一个台阶:
| 成本项 | 影响 |
|---|---|
| 代码路径 | 每个 bugfix 要在多版本重复 |
| 文档 | 需要维护多套说明 |
| 测试 | 多版本回归矩阵膨胀 |
| 数据模型 | 不同版本的字段映射带来转换层 |
| 运维认知 | 排查问题要先确认在哪个版本 |
经验值:同时维护超过 2 个版本,通常意味着前面的废弃流程没做好。
务实的做法是:默认只维护一个稳定版本 + 一个即将上线的版本;老版本给出明确的下线时间表。允许"多版本共存"不等于"多版本永久共存"。
Schema 演进:可选字段与枚举扩展
在不破坏兼容的前提下演进数据结构,有几条安全规则:
text
✓ 字段只增不删(要删先废弃再在新版本移除)
✓ 新字段设为可选,给默认值
✓ 枚举只追加新值,不删旧值
✓ 用包装类型表达"可能没有",避免 null 歧义枚举扩展是最容易翻车的:服务端加了一个 status: "archived",老客户端的 switch 没有 default 分支,直接崩了或把未知值当错误处理。约定"未知枚举值必须被容忍",是跨版本演进能成立的前提。
与客户端协商
版本不是服务端单方面的事。要让客户端在请求里带上自己能理解的版本,并在响应里告诉客户端"这些字段即将废弃"。常用的协商手段:
| 方式 | 谁主导 | 适合 |
|---|---|---|
| 客户端指定 URL 版本 | 客户端 | 明确、可控 |
| 服务端按客户端能力返回 | 服务端 | 客户端无感知升级 |
| 响应头提示废弃 | 双方 | 配合迁移周期 |
无论用哪种,核心是让"谁在用哪个版本"可观测。没有这层可见性,废弃和下线就只能靠猜。
务实的检查清单
- 区分破坏性变更和兼容性扩展,只在破坏时才动用版本。
- 优先 URL 版本,调试和工具链支持都更好。
- 默认客户端"忽略未知字段",这是向后兼容的地基。
- 废弃要走"标记 → 观察 → 迁移 → 移除"流程,绝不直接删。
- 限制同时维护的版本数,超过 2 个通常是废弃流程出了问题。
- 枚举只追加不删除,并约定未知值必须被容忍。
- 让"谁在用哪个版本"可观测,否则下线只能靠猜。