关于 Rust 错误处理,我是被现实毒打才开窍的

举报
行天 发表于 2026/11/01 19:33:25 2026/11/01
【摘要】 说出来不怕丢人,我写 Rust 的前三个月,错误处理全靠 unwrap。let x = foo().unwrap(),一路 unwrap 到 main,心想反正是个 demo,能跑就行。然后这个 demo 上线了。那是个内部用的小工具:读配置文件、调几个 HTTP 接口、结果写 SQLite。某天早上运维在群里 @我,说工具在他机器上一闪而过,啥也没留下。我让他把报错贴出来,就一行:thre...

说出来不怕丢人,我写 Rust 的前三个月,错误处理全靠 unwrap。let x = foo().unwrap(),一路 unwrap 到 main,心想反正是个 demo,能跑就行。

然后这个 demo 上线了。

那是个内部用的小工具:读配置文件、调几个 HTTP 接口、结果写 SQLite。某天早上运维在群里 @我,说工具在他机器上一闪而过,啥也没留下。我让他把报错贴出来,就一行:

thread 'main' panicked at 'called Result::unwrap() on an Err value: Os { code: 2, kind: NotFound ... }', src/main.rs:42:38

我打开 main.rs 第 42 行,是读配置文件的 unwrap。原因蠢到离谱:他机器上配置文件名大小写写错了。就这么个破事,工具直接 panic,连句人话都没留。那天我改 bug 的时候就在想,这要是正经项目,我已经被开除了。

(顺便吐槽:code: 2 这种东西,第一次见根本反应不过来是"文件不存在",Windows 本地化之后的报错信息更是难读。这都是后话。)

Box<dyn Error>:看起来很美,实际很坑

我的第一版重构,是把所有函数签名改成 -> Result<T, Box<dyn std::error::Error>>。? 随便用,编译全过,我当时觉得自己悟了。

悟了个锤子。

两周后我要加个功能:配置文件不存在时,自动生成一份默认配置继续跑。用 Box<dyn Error> 我怎么写?if err.to_string().contains("找不到")?写出这行代码的时候,我盯着屏幕,感觉自己在写十年前的祖传代码。Rust 的类型系统是拿来用的,不是拿来供着的,我却亲手把它扔了。

anyhow:先拿来救急

朋友推荐了 anyhow。说实话第一眼我是拒绝的——又多一个依赖?但用了十分钟就真香了。核心就一个东西:context。

fn load_config(path: &str) -> anyhow::Result<Config> {
    let text = std::fs::read_to_string(path)
        .with_context(|| format!("读配置文件 {path} 挂了"))?;
    toml::from_str(&text).context("配置文件 TOML 解析挂了")
}

报错长这样:

Error: 读配置文件 config.toml 挂了

Caused by:
    系统找不到指定的文件。 (os error 2)

从此线上出问题,我先看这条链,十次有九次不用复现就能定位。我现在写应用代码有个死规矩:离错误源头最近的地方加 context,中间层闭嘴透传,main 里统一打印。谁 review 我代码发现裸 ? 跨了三层还没 context,奶茶他请。

哦对,anyhow 还有个 bail!,bail!("这配置我属实看不懂: {path}"),比 return Err(anyhow!(...)) 省按键,这种小甜点用上就回不去了。

什么时候必须换 thiserror

anyhow 有个原罪:它把错误的具体类型吃了。写应用无所谓,反正最后都是打印给人看。但写库不行。

我后来把配置加载抽成内部 crate 给组里另一个项目用,人家提需求:"文件不存在我要 fallback,TOML 写错了我要报错退出。"我看着手里的 anyhow::Error,心想这让我怎么分?又回去字符串匹配?

于是有了这段:

#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
    #[error("配置文件找不着: {0}")]
    NotFound(String),
    #[error("TOML 写错了")]
    BadToml(#[from] toml::de::Error),
    #[error("IO 挂了")]
    Io(#[from] std::io::Error),
}

#[from] 是真省事,From 实现不用手写。调用方终于能 match 了:

match load_config(path) {
    Err(ConfigError::NotFound(_)) => Config::default(), // 没有就用默认的顶着
    Err(e) => anyhow::bail!("配置加载失败: {e:#}"),
    Ok(c) => c,
}

{e:#} 这个写法安利一下,anyhow 的 alternate 打印,能把整条因果链打出来,看日志的时候好用。

我的分层习惯,仅代表个人

库用 thiserror,错误类型就是 API 的一部分,改它等于 breaking change,定之前想清楚。应用用 anyhow,context 加足,别心疼那几行代码。别混着来,更别在库的公开函数里返回 anyhow::Result,一文不值,被同事在评审会上当众处刑。现在学乖了。

还有,unwrap 不是原罪。我现在还会在两种地方写:硬编码的字符串解析("127.0.0.1".parse().unwrap(),这要能错那宇宙都错了),和测试代码。除此之外再看见 unwrap,我配了 clippy::unwrap_used,谁写谁修。

行了,就写这么多。下次想聊 tracing,那又是一部血泪史。


原创声明:本文为本人原创,转载请注明出处。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。