高级 Rust 开发实践
经过实战检验的 Rust 工作区架构、代码组织、依赖管理和测试模式,从原型到生产都能扩展。
Git 工作树工作流程合规性
所有编码工作必须在 git 工作树中进行。 在进行任何代码更改之前:
- 创建一个工作树:
git worktree add ~/.claude/worktrees/$(basename $(pwd))/<task> -b feat/<task> - 在该目录中工作
- 使用
/merge将更改合并回主线
永远不要直接在主工作树中编辑文件。
完成要求
在完成任何 Rust 任务之前,你必须:
- 运行测试:
cargo test --workspace - 运行 linting:
trunk check - 在声明完成之前修复任何问题
如果 trunk 有格式化问题,请运行 trunk fmt 自动修复。
工作区架构
从 “一个产品 = 一个仓库 = 一个工作区” 开始
当你有以下情况时,使用 Rust 工作区:
- 多个一起交付的 crates(二进制文件 + 库)
- 共享工具 / CI
- 共享版本策略
规范的工作区结构:
repo/
Cargo.toml # 工作区根
crates/
core/ # 纯领域逻辑(无 IO)
storage/ # DB、文件系统等
api/ # HTTP/GRPC 处理程序、DTO
cli/ # 二进制文件
tools/ # 可选:内部二进制文件(代码生成、迁移等)
tests/ # 可选:黑盒集成测试
保持 Crates ‘薄’ 和边界 ‘硬’
分层架构:
-
core: 纯逻辑、类型、验证、算法。最少的依赖。
-
adapters: IO 边界(数据库、网络、RPC、文件系统)。基于 Trait 的边界,最小泄漏。
-
app / service: 连接(DI)、配置、运行时、编排。
-
bins: 仅调用 ‘app’ 的 CLI/守护进程。
关键规则: 如果 core 导入了 tokio、reqwest 或 sqlx,你已经失去了分离。
默认使用少量的 Crates
太多的 crates 是忙碌的工作。从最多 2-5 个开始。
仅在以下情况下拆分:
- 编译时间痛苦且边界真实
- 你需要不同的发布节奏
- 你需要不同的依赖配置文件(无 std、wasm 等)
工作区依赖项:集中版本,而不是架构
在根 Cargo.toml 中,使用工作区依赖项以保持版本一致:
[workspace]
members = ["crates/*"]
resolver = "2"
[workspace.dependencies]
anyhow = "*" # 使用最新版本
thiserror = "*" # 使用最新版本
serde = { version = "*", features = ["derive"] } # 使用最新版本
tokio = { version = "*", features = ["macros", "rt-multi-thread"] } # 使用最新版本
在 crate Cargo.toml 中:
[dependencies]
serde = { workspace = true }
这减少了版本漂移和安全动荡。
对功能毫不留情
- 优先选择增加功能(启用更多能力)而不是 “改变语义的特性标志”
- 将 ‘重’ 依赖放在特性后面(数据库、http、指标)
- 避免默认特性拉取整个世界
可选依赖项的模式:
[dependencies]
sqlx = { workspace = true, optional = true }
[features]
db = ["dep:sqlx"]
执行政策:MSRV + 工具链
- 用
rust-toolchain.toml固定工具链 - 决定 MSRV(最小支持的 Rust 版本)并在 CI 中测试
- 保持 clippy/rustfmt 一致
代码组织
模块应该匹配你的推理方式,而不是文件每类型
按能力/领域组织,而不是按 “models/handlers/utils” 意大利面。
良好的组织:
core/
src/
lib.rs
payment/
mod.rs
validation.rs
pricing.rs
user/
mod.rs
id.rs
rules.rs
避免:
models.rs
handlers.rs
utils.rs
公共 API:小表面积,明确的重新导出
- 默认情况下使大多数东西
pub(crate) - 从
lib.rs重新导出一个策划的 API
mod payment;
pub use payment::{Payment, PaymentError};
如果一切都是 pub,你就创建了一个意外的框架。
除非你真的需要,否则避免 “Prelude”
Preludes 倾向于隐藏依赖并使代码审查更加困难。更喜欢显式导入。
错误策略:选择一个并坚持下去
常见方法:
-
库 crates:
thiserror用于类型化错误 -
二进制文件:在顶层使用
anyhow
除非你明确想要 “不透明”,否则不要跨库边界泄露 anyhow::Error。
保持异步在边缘
如果你能保持核心同步和纯净,你将获得:
-
更简单的测试
-
可移植性
-
减少生命周期/固定头痛
依赖卫生
挑剔:更少的依赖,更高质量的依赖
每个依赖都增加了:
-
构建时间
-
审计表面
-
Semver 风险
更喜欢有强大维护的 “无聊” crates。
使用 cargo-deny + cargo-audit
尽早使依赖问题可见(许可证、咨询、重复版本)。
不要在库中使用 unwrap()
在二进制文件/测试中没问题(特别是在测试脚手架中)。在库中,用上下文返回错误。
测试策略扩展
考虑 “金字塔”:
1. 单元测试:快速、确定性、很多
-
将大多数测试放在代码附近:
mod tests {}在同一文件中用于私有访问 -
测试不变性和边缘情况,不仅仅是快乐路径
-
避免在单元测试中击中文件系统/网络
2. 集成测试:黑盒公共 API
使用 crates/<crate>/tests/*.rs 进行 API 级测试。
-
将其视为 “crate 的消费者”
-
不要深入私有内部
3. 端到端测试:少数,但真实
如果你有一个服务:
-
在 CI 中启动依赖项(数据库)(容器)
-
运行一小套场景测试
4. 属性测试 + 模糊测试当正确性很重要时
-
proptest用于不变式(“decode(encode(x)) == x”) -
cargo-fuzz用于解析器/解码器/来自外部的输入
5. Doctests 被低估了
Doctests 强制执行示例编译并保持你的公共 API 诚实。
日志记录和跟踪
永远不要使用 println! - 使用跟踪代替
永远不要使用 println!、eprintln! 或 dbg! 进行输出。 总是使用 tracing crate:
use tracing::{debug, info, warn, error, trace};
// 好的 - 结构化日志记录
info!("Processing request for user {user_id}");
debug!("Cache hit: {key}");
warn!("Retry attempt {attempt} of {max_retries}");
error!("Failed to connect: {err}");
// 坏的 - 永远不要这样做
println!("Processing request for user {}", user_id);
dbg!(value);
为什么:
-
具有级别的结构化日志记录(在生产中过滤噪音)
-
分布式跟踪的跨度
-
可配置的输出(JSON、漂亮等)
-
当禁用时零成本
为测试使用 test-log
总是使用 test_log::test 属性来捕获跟踪输出:
use test_log::test;
#[test]
fn test_something() {
info!("This will be visible when test fails or with --nocapture");
assert!(true);
}
#[test(tokio::test)]
async fn test_async_something() {
debug!("Async test with tracing");
}
添加到 Cargo.toml(使用最新版本):
[dev-dependencies]
test-log = { version = "*", features = ["trace"] } # 使用最新版本
tracing-subscriber = { version = "*", features = ["env-filter"] } # 使用最新版本
用可见日志运行测试:RUST_LOG=debug cargo test -- --nocapture
Clippy 规则要遵循
内联格式参数(clippy::uninlined_format_args)
总是直接在格式字符串中使用变量,而不是将它们作为参数传递:
// 好的 - 变量内联
let name = "world";
info!("Hello, {name}!");
format!("Value: {value}, Count: {count}")
// 坏的 - 未内联参数
info!("Hello, {}!", name);
format!("Value: {}, Count: {}", value, count)
这提高了可读性并减少了潜在的参数顺序错误。
CI / 质量门(最小设置)
cargo fmt --check
cargo clippy --all-targets --all-features -D warnings
cargo test --workspace --all-features
其他门:
-
MSRV 检查(如果你声称一个)
-
cargo deny/cargo audit -
(可选)
cargo llvm-cov用于覆盖率,但不要崇拜 %
编译时间和人体工程学
-
使用
resolver = "2"并避免不必要的默认特性 -
如果它们主导重建时间,将 “重型” crates(如数据库代码生成、protobuf)分割成单独的 crates
-
偏好增量友好模式:较少的 proc-macros,较少的泛型在热路径中除非需要
实用的经验法则
单向依赖:
-
core→ (无) -
adapters→core -
app→adapters+core -
bin→app
可见性:
-
默认情况下一切都是私有的
-
公共 API 是一个深思熟虑的设计工件
IO 放置:
core中没有 IO
测试分布:
-
到处都有单元测试
-
在边界处进行集成测试
-
少量 E2E 测试
工具:
-
固定工具链
-
集中化版本
-
功能警察
项目类型模式
CLI: 薄二进制文件 → 库(为了可测试性)
服务: 分开的协议定义;功能标志传输层
ZK/crypto: 隔离 no_std 核心;分开的证明/验证 crates
WASM: 分开的绑定;平台不可知核心