关于 Rust 错误处理,我是被现实毒打才开窍的
说出来不怕丢人,我写 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,那又是一部血泪史。
原创声明:本文为本人原创,转载请注明出处。
- 点赞
- 收藏
- 关注作者
评论(0)