← Rust:从入门到精通
全栈开发Rust:从入门到精通

生态选型与远航:从 cargo new 到 Rustacean

Rust 从入门到精通系列第 14 篇(收官):起手栈各领域的事实标准、thiserror 与 anyhow 的分界线实证、如何读一个 crate 的尽调五信号、十四篇的完整回望,以及一个 Rustacean 接下来的路。

RustEcosystemCratesthiserroranyhowCareer

十四篇之前,你 cargo new 出第一个项目;现在,所有权、类型系统、并发、unsafe、宏和性能工程都已在你的工具箱里。最后一块拼图不是语言——Rust 的哲学“语言给机制,生态给实现”意味着选型是日常技能:标准库刻意留白的地方(执行器、web、序列化),由 crates.io 上二十万个 crate 填上。收官篇讲三件事:起手栈的事实标准、判断一个 crate 值不值得依赖的尽调方法,以及回头看这十四篇走过的完整弧线——然后把你送出港。

起手栈:每个领域的事实标准

生态在变,但骨架惊人地稳定。这些是“默认选择”,偏离它们需要理由,而不是反过来:

  • tokio——异步运行时的事实标准(第 10 篇执行器的工业版)。选它的理由和选 Linux 一样:生态位即护城河。
  • axum——tokio 团队自家的 web 框架:路由、提取器、中间件全部类型化,和第 5、7 篇的 trait/泛型无缝咬合。
  • serde——序列化的同义词。第 12 篇的 derive 原理放大一万倍,就是它。
  • clap——CLI 参数解析:把命令行接口声明成一个 struct,derive 出全部解析逻辑。
  • thiserror / anyhow——错误处理的两半,下一节专门还这笔债。
  • tracing——结构化日志与 span,async 感知,println! 调试的成人版。
  • criterion / proptest——基准测试(第 13 篇)与属性测试:后者自动生成几百个用例攻击你的函数,是第 4 篇穷尽性精神在测试领域的亲戚。

学每个的形状就够:它们解决什么问题、核心抽象是什么。具体 API 查文档——第 1 篇说过,知道答案住在哪里是学习的一半。

偿还第 4 篇的债:thiserror 与 anyhow 的分界线

第 4 篇立过一条规则:“库用结构化错误,应用用聚合错误”,分界线是一个问题——调用者需不需要区分你的错误? 现在看这两个 crate 如何把规则变成零样板。

库的一侧,thiserror:你写 enum 和语义,它生成 DisplayErrorFrom 的全部实现:

#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
    #[error("failed to read {path}")]
    Io { path: String, #[source] source: std::io::Error },
    #[error("invalid port {0}: must be 0..=65535")]
    BadPort(u32),
}

fn parse_port(raw: &str) -> Result<u16, ConfigError> {
    let n: u32 = raw.parse().map_err(|_| ConfigError::BadPort(u32::MAX))?;
    u16::try_from(n).map_err(|_| ConfigError::BadPort(n))
}

#[error("...")] 里的插值直接引用字段,#[source] 接起错误链——调用者拿到的是可以 match 的精确错误(“BadPort 就提示用户改配置,Io 就重试”),这正是“结构化”的含义。实测输出:invalid port 99999: must be 0..=65535

应用的一侧,anyhow:一个 anyhow::Result<T> 兜住所有错误, .context() 沿途加注, {:#} 打印整条因果链:

fn load_config() -> anyhow::Result<u16> {
    let raw = std::fs::read_to_string("port.txt")
        .map_err(|e| ConfigError::Io { path: "port.txt".into(), source: e })?;
    let port = parse_port(raw.trim())?;
    Ok(port)
}
// 出错时打印:failed to read port.txt: No such file or directory (os error 2)

注意两侧可以和平共处:应用内部的库代码用 thiserror 保持精确,顶层用 anyhow 聚合上报——分界线不是项目边界,而是“这个错误的观众是谁”。

如何读一个 crate:尽调五信号

添加一个依赖就是雇佣一个长期室友——它住进你的构建、你的依赖树、你的供应链。下载量只是入场券(它证明流行,不证明健康),决定录用的是五个信号:

  1. 脉搏:最近一次 commit 和 release 是什么时候?活着不等于频繁更新——成熟的小 crate 几年不动可能是“完成”,不是“死亡”;区分方法是看 issue 区有没有人回应。
  2. 响应:issue 和 PR 有人处理吗?维护者的带宽是这个 crate 最稀缺的资源。
  3. 体重cargo tree(第 8 篇)——它会带多少个 crate 一起住进来?一个解析 TOML 的库拖进 80 个依赖,是供应链审查会卡住的那种体重。
  4. 文档:docs.rs 首页就是 README 吗?示例能编译吗?(能——第 8 篇的 doc test 保证。)文档是维护者如何对待用户的切片样本。
  5. 表面:unsafe 用量与位置(边界画法对不对,第 11 篇的眼睛)、license 与你的项目兼容吗、MSRV(最低支持的 Rust 版本)卡不卡你的工具链。

五分钟的尽调,省的是半年后替换依赖的两周。

回望:十四篇,一条不变式

从第 1 篇到现在,表面上是十四个主题,底下其实只有一句话在反复出现,它配得上写在系列的墓碑上:把不变式从注释搬进类型,让编译器替你记住。 所有权是它在内存上的样子(第 2–3 篇),enum 与穷尽匹配是它在领域上的样子(第 4 篇),trait 与泛型是它在行为上的样子(第 5、7 篇),智能指针是它的价目表(第 6 篇),Cargo 与模块是它在工程上的样子(第 8 篇),无畏并发是它最辉煌的胜仗(第 9–10 篇),unsafe 与宏是它诚实标出的边界(第 11–12 篇),性能工程是它的账单明细(第 13 篇)。借用检查器从对手变成导师的那一刻,就是这条不变式长进你手里的那一刻。

接下来去哪?no_std 嵌入式(所有权在裸金属上一样锋利)、WebAssembly(第 1 篇说的“越来越多的负载”)、内核与驱动、或者就回到你自己的项目——写一个小服务、一个 CLI、一个解析器,发布它,维护它。练习册到此为止,剩下的路用代码写。

最后一组练习,以及远航

  1. 毕业设计:一个 todo CLI——clap 解析子命令(add/list/done),serde 把任务持久化成 JSON,anyhow 聚合错误,tracing 记日志。两百行以内,但这两百行里有整个系列。
  2. 尽调实战:挑三个你准备用的 crate,跑一遍五信号,每个写五行结论。你会发现其中一个配不上它的下载量——这就是方法生效的瞬间。
  3. 读源码:打开 itertools 或 bytes 的源码读一小时。看不懂的地方回到对应那一篇——系列的每一篇都是为“读得懂真实代码”而写的。

系列到这里收官。谢谢你把十四篇读完——现在,去写那个你不再害怕半夜告警的服务吧。

guest@swangnice:~$