「深挖Rust」错误处理 — 1

1,518 阅读2分钟

「这是我参与2022首次更文挑战的第 2 天,活动详情查看:2022首次更文挑战」。


为了可以正确发送一封邮件,你必须把多个操作串联一起,组成一个大的函数:验证用户输入、发送邮件、各种数据库查询。这些操作它们都有一个共同点:它们都可能会失败。

前面我们讨论了Rust中错误处理的基石:Result 以及 ?

我们留下了许多问题没有回答:

  1. 错误如何契合应用程序中更广泛的体系?
  2. 一个好的错误表达是什么样子的?
  3. 错误是为谁准备的?
  4. 我们应该使用库吗?使用的话又是哪一个?

那下面就让我们一起深入分析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