senior-rust-practicesSkill senior-rust-practices

这个技能用于指导高级 Rust 开发模式,包括工作区设计、代码组织策略、生产就绪的 Rust 架构。关键词包括 Rust 工作区、Rust 最佳实践、Cargo 工作区设置、Rust 代码组织、Rust 依赖管理、Rust 测试策略、Rust 项目、可扩展的 Rust、Rust CI 设置。

架构设计 0 次安装 0 次浏览 更新于 3/2/2026

高级 Rust 开发实践

经过实战检验的 Rust 工作区架构、代码组织、依赖管理和测试模式,从原型到生产都能扩展。

Git 工作树工作流程合规性

所有编码工作必须在 git 工作树中进行。 在进行任何代码更改之前:

  1. 创建一个工作树:git worktree add ~/.claude/worktrees/$(basename $(pwd))/<task> -b feat/<task>
  2. 在该目录中工作
  3. 使用 /merge 将更改合并回主线

永远不要直接在主工作树中编辑文件。

完成要求

在完成任何 Rust 任务之前,你必须:

  1. 运行测试:cargo test --workspace
  2. 运行 linting:trunk check
  3. 在声明完成之前修复任何问题

如果 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 导入了 tokioreqwestsqlx,你已经失去了分离。

默认使用少量的 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 → (无)

  • adapterscore

  • appadapters + core

  • binapp

可见性:

  • 默认情况下一切都是私有的

  • 公共 API 是一个深思熟虑的设计工件

IO 放置:

  • core 中没有 IO

测试分布:

  • 到处都有单元测试

  • 在边界处进行集成测试

  • 少量 E2E 测试

工具:

  • 固定工具链

  • 集中化版本

  • 功能警察

项目类型模式

CLI: 薄二进制文件 → 库(为了可测试性)

服务: 分开的协议定义;功能标志传输层

ZK/crypto: 隔离 no_std 核心;分开的证明/验证 crates

WASM: 分开的绑定;平台不可知核心