如何废弃 API?

解读

在国内 Rust 岗位面试中,面试官提出“如何废弃 API”并不是想听你背文档,而是考察三点:对 Rust 稳定性承诺(Stability Promise)的理解对 Cargo/SemVer 工作流的熟悉度对团队协作与下游用户体验的敏感度。答得太浅(只说“打个注释”)会被认为没做过生产级库;答得太深(直接聊 RFC 流程)又容易跑题。最佳策略是:用标准库和主流 crates.io 生态的真实做法做骨架,再补充国内企业内部私有仓库的落地经验,让答案既合规又接地气

知识点

  1. 语义化版本(SemVer):rust 生态强制遵循 SemVer,废弃动作必须落在 “小版本(minor)” 里,保证 0.x 或 1.x 现有代码继续编译。
  2. #[deprecated] 属性
    • 语法:#[deprecated(since = "1.2.3", note = "请用 xxx 替代")]
    • 支持函数、结构体、枚举、trait、宏、甚至整条模块。
    • 默认告警等级为 allow,需显式 #[warn(deprecations)]deny 才能阻断 CI。
  3. cargo 工作流
    • cargo build --all-targets 会拉取最新 deprecation warning;
    • cargo clippy -- -D deprecated 可把告警变错误,防止国内团队“warning 太多没人看”。
  4. 文档废弃
    • 在 lib.rs 顶部加 #![doc(issue_tracker_base_url = "https://jira.xxx.cn")],方便把 deprecated 项统一指向内部工单;
    • #[doc(hidden)] 隐藏实现细节,但仍保留导出,保证老二进制链接得上。
  5. 私有 registry 的“灰度下架”
    • 国内很多企业用 nexus / verdaccio 搭建私有 crates 代理;
    • 废弃后先 yank(cargo yank --vers 1.2.3),阻止新项目依赖,但老项目 lock 文件继续可用;
    • 等内部所有仓库 CI 通过新版后,再物理删除 .crate 文件,完成“软下架→硬下架”。
  6. 沟通机制
    • 在 CHANGELOG.zh.md 里用“⚠️ 废弃公告”二级标题,写清迁移脚本;
    • 飞书/企业微信群里发“废弃巡检机器人”,每天统计仍未整改的仓库,把技术债变成 OKR

答案

分四步走,全部在 minor 版本完成,绝不 break 现有用户:

  1. 加属性发告警:在即将废弃的 API 上统一加 #[deprecated(since = "x.y.z", note = "迁移指南见 https://wiki.xxx.cn/rust-migration")];同时把 clippy::deprecated 提升为 deny,让国内 CI 第一时间爆红,防止“warning 淹没”。
  2. 发 minor 版本并 yank:执行 cargo publish 后立刻 cargo yank --vers x.y.z,使 crates.io/私有 registry 不再向“新项目”解析该版本,但老 lock 文件继续可用,符合国内金融场景“热补丁”需求
  3. 文档与自动化
    • 在 lib.rs 顶部加 #![cfg_attr(feature = "deprecated-cleanup", deny(deprecations))],提供一个 feature 闸口,方便业务线批量整改后一次性开启 deny;
    • rustdoc --document-private-items 生成私有文档,把 deprecated 项集中列在“废弃页”,降低新人踩坑率。
  4. 制定清除节奏
    • 在内部 confluence 建立“废弃 API 生命周期看板”,列清每个 deprecated 项的“提出时间、迁移责任人、预计删除版本”;
    • 至少经历 一个 major 版本周期(如 1.x → 2.x) 后,在下一个 major 里彻底删除代码,同时更新 README:“如仍需旧 API,请锁版本 1.x 并自行维护分支”。

一句话总结:“先告警、再下架、后删除,全程 SemVer 留痕,让编译器帮你背锅。”

拓展思考

  1. 异步生态的废弃陷阱tokio 曾经废弃 tokio::spawn(async {}) 的某些旧签名,却因下游大量 proc-macro 依赖而不得不回滚。结论——对外暴露的宏里隐藏 deprecated 项会指数级放大迁移成本,国内团队在设计 crate 边界时应尽量“宏最小化”。
  2. FFI 废弃:如果 API 已通过 #[no_mangle] 导出给 C++/Go,deprecated 属性不会穿透到动态链接层。此时要在 .h 文件里加 __attribute__((deprecated)),并同步发 major 版本才能删除符号,否则会出现国产麒麟系统“升完 so 直接 coredump”的悲剧。
  3. 灰度编译:大型国企代码量超千万行,可写 cargo plugin 做“条件编译废弃”——
    #[cfg_attr(feature = "deprecated-allow", deprecated)]
    
    让各业务线先在灰度环境打开 feature 验证,再全量切换,把“编译通过即正确”升级为“编译通过即灰度正确”