什么时候该把一个 crate 拆成 workspace
拆 workspace 的触发条件是编译时间和依赖边界,不是目录好不好看。三个信号成立才拆,否则只是把一次编译换成多个包的管理成本。
「要不要拆 workspace」的答案不该来自审美。目录再整齐,只要编译没变快、依赖没变干净,拆开就只增加了管理成本。
三个信号
- 改一处要重编全部:只改了 CLI 的参数解析,却要重新编译整个 crate(包括几百个依赖项的那个核心库)。增量编译失效是拆分的第一个真实理由。
- 依赖集合差异大:一边需要 tokio 的完整 runtime、HTTP 客户端、TLS,另一边只是几个纯函数的解析器。放在一个 crate 里,后者的使用者被迫拉上前者全部依赖。
- 需要暴露稳定子集:核心库要给外部用户当库用,而 CLI 是内部工具。同一个 crate 里,内部实现细节很容易因为方便而变成
pub。
三个都不成立就别拆。几十个源文件的 crate 编译是秒级,拆成四个包之后,cargo test 要在四个包上跑、版本要一起升、模块路径从 crate::foo 变成 dep::foo。
拆法
根 Cargo.toml 只放成员与统一版本:
[workspace]
members = ["crates/core", "crates/protocol", "crates/cli"]
resolver = "2"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
成员的依赖写成 serde.workspace = true,内部依赖用 path。这样升级一个依赖只需要改一处 —— 这是拆开之后唯一确定会变好的部分。
拆完才会遇到的四个问题
- 循环依赖没有优雅解:A 需要用 B 的类型、B 需要用 A 的类型时,只能再抽一个 crate C 放公共类型,于是依赖图变成菱形。这种情况出现两次以上,说明拆分线划错了。
pub会失控:跨 crate 之后一切都得pub,而pub(crate)的边界消失了。内部类型泄漏成公开 API 是迟早的事,得靠#[doc(hidden)]和明确的模块组织去挡。- 内部 crate 不该发布:
publish = false一行能省掉很多版本号同步的麻烦。要发布的只有真正给外部用的那个。 - feature 会组合爆炸:A 的 optional 依赖变成 B 的 feature,B 的 feature 又要转发给 C。能不转发就不转发,宁可让最上层显式依赖。
判断标准
我用的标准很土:能写出一句「不拆会怎样」就拆,写不出来就别拆。「不拆的话每次改 CLI 都要等 40 秒」是理由;「拆开更清晰」不是。
先按模块分文件。等到编译时间真的开始影响你保存文件的频率,再动手拆 crate。

评论
…