「这是我参与2022首次更文挑战的第 2 天,活动详情查看:2022首次更文挑战」。
为了可以正确发送一封邮件,你必须把多个操作串联一起,组成一个大的函数:验证用户输入、发送邮件、各种数据库查询。这些操作它们都有一个共同点:它们都可能会失败。
前面我们讨论了Rust中错误处理的基石:Result
以及 ?
。
我们留下了许多问题没有回答:
- 错误如何契合应用程序中更广泛的体系?
- 一个好的错误表达是什么样子的?
- 错误是为谁准备的?
- 我们应该使用库吗?使用的话又是哪一个?
那下面就让我们一起深入分析Rust中错误处理模式。
Error 出现的本质
我们先看一个例子:
//! src/routes/subscriptions.rs
// [...]
pub async fn store_token(
transaction: &mut Transaction<'_, Postgres>,
subscriber_id: Uuid,
subscription_token: &str,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO subscription_tokens (subscription_token, subscriber_id)
VALUES ($1, $2)
"#,
subscription_token,
subscriber_id
)
.execute(transaction)
.await
.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
e
})?;
Ok(())
}
我们试图往 table subscription_tokens
中插入一行,用来存储 subscriber_id
对应的新生成token。 execute
是一个容易出错的操作:比如在与数据库会话时可能有网络问题,比如我们试图插入的行可能违反了一些表的约束(例如主键的唯一性),等等各种原因。
Internal Errors
execute
的调用者可能希望在失败发生时得到通知 —— 他们需要对其做出相应的反应,例如:重试查询,或者像我们的例子中那样用 ?
。
Rust利用类型系统来告知操作可能不会成功:execute
的返回类型是 Result
,一个枚举。
pub enum Result<Success, Error> {
Ok(Success),
Err(Error)
}
然后,Rust编译器迫使调用者写明他们打算如何处理这两种情况 —— 成功或是失败。
如果我们的目标是向调用者传达发生了错误,我们可以使用一个更简单的 Result
定义:
pub enum ResultSignal<Success> {
Ok(Success),
Err
}
这样就不需要泛型的 Error
类型,我们可以只需要检查 execute
返回是不是 Err
,例如:
let outcome = sqlx::query!(/* ... */)
.execute(transaction)
.await;
if outcome == ResultSignal::Err {
// Do something if it failed
}
不过如果只有这一种故障模式,上述是可行。但事实是,操作可能会以各种方式失败,我们可能想根据发生的情况做出不同的反应。 让我们看看 sqlx::Error
的框架,也就是 execute
的错误类型:
//! sqlx-core/src/error.rs
pub enum Error {
Configuration(/* */),
Database(/* */),
Io(/* */),
Tls(/* */),
Protocol(/* */),
RowNotFound,
TypeNotFound {/* */},
ColumnIndexOutOfBounds {/* */},
ColumnNotFound(/* */),
ColumnDecode {/* */},
Decode(/* */),
PoolTimedOut,
PoolClosed,
WorkerCrashed,
Migrate(/* */),
}
sqlx::Error
设计成一个 enum
,调用者可以匹配返回的错误,并根据底层的错误分类表现出不同的行为。例如,你可能希望重试 PoolTimedOut
,或者是你可能会忽略 ColumnNotFound
。