blog.dopana

Back

Rust 的错误处理机制与大多数主流编程语言截然不同。Rust 没有 try/catch 异常机制,也没有 null 指针。相反,它让错误处理变得显式、类型安全,并在编译阶段就接受严格的合法性校验。

像给 10 岁孩子解释:邮局快递派送系统#

把 Rust 中的函数调用想象成寄送一份快递包裹:

  1. 派送成功 (Ok(包裹)): 快递员按响门铃,将完好无损的商品亲手交到你手上。
  2. 可处理的派送问题 (Err(原因)): 地址门牌号模糊或收件人暂时不在家。快递站将通知单回传给寄件人,由寄件人决定下一步操作(重新派送、电话核对或转寄)。
  3. 不可抗力的灾难事故 (panic!): 运输货车发生严重爆炸,或者整栋邮局大楼倒塌。由于失去了任何安全继续的可能,整个运转流程立即强制终止。
graph TD
    Op["操作 / 函数调用"] --> Choice{"错误是否可以恢复?"}
    Choice -->|否: 严重 Bug / 破坏不变性| Panic["panic! 宏<br/>(崩溃, 栈展开, 退出进程)"]
    Choice -->|是: 文件缺失, 网络超时, 格式错误| ResultEnum["Result&lt;T, E&gt;"]
    ResultEnum --> OkBranch["Ok(值) -> 成功处理"]
    ResultEnum --> ErrBranch["Err(错误) -> 局部处理或使用 ? 向上抛出"]

Rust 中的两大错误类别#

特性可恢复错误 (Result<T, E>)不可恢复错误 (panic!)
机制枚举 Result<T, E> (OkErr)panic!("...")
典型原因文件不存在、数值解析失败、网络超时数组索引越界、断言失败、不可恢复的不变性破坏
处理手段matchif let? 运算符、组合子终止进程或栈展开清理资源
设计哲学属于程序正常业务流程中的可预见分支严重代码缺陷;程序已无法安全可靠地运行

使用 panic! 处理不可恢复错误#

panic! 被触发时:

  1. 错误信息会被打印到标准错误流 stderr
  2. 默认情况下进行栈展开(unwind)——沿着调用栈回溯清理内存与资源。
  3. 程序以非零状态码退出。
src/main.rs
fn main() {
    // 主动触发 panic
    // panic!("系统故障:数据库连接池状态已被破坏");

    // 运行时检查触发的隐式 panic
    let numbers = vec![10, 20, 30];
    let _item = numbers[99]; // panics: index out of bounds: the len is 3 but the index is 99
}
rust

[!NOTE] 在终端中设置环境变量 RUST_BACKTRACE=1,可在程序发生 panic 时打印出详尽的调用栈回溯信息,准确定位出错源码位置。

使用 Result<T, E> 处理可恢复错误#

Rust 标准库预导入模块中内置定义了 Result

enum Result<T, E> {
    Ok(T),
    Err(E),
}
rust

使用 match 进行多层模式匹配#

解包快捷方式:unwrapexpect#

在编写测试用例或快速原型开发时,Result 提供了方便快捷的取值方法:

src/main.rs
use std::fs::File;

// 1. .unwrap() -> 若为 Ok 则返回值,若为 Err 则触发 panic 并输出默认信息
let f1 = File::open("config.json").unwrap();

// 2. .expect(msg) -> 若为 Ok 则返回值,若为 Err 则触发 panic 并附带自定义上下文
let f2 = File::open("config.json").expect("config.json 是应用启动的必要配置文件");
rust

[!TIP] 在生产级代码中,务必优先使用 .expect() 而非 .unwrap()。具备上下文意义的错误信息能极大缩短排查生产日志的时间。

使用 ? 运算符传递错误#

与其每次都用重复的 match 样板代码抛出错误,? 运算符能够自动解包 Ok(T),或在遇到 Err(E) 时立刻从当前函数提前返回:

sequenceDiagram
    participant Caller as 上层调用函数
    participant Worker as read_username
    participant FS as 文件系统

    Worker->>FS: File::open("username.txt")
    alt 文件存在
        FS-->>Worker: Ok(file)
        Worker->>FS: read_to_string()
        alt 读取成功
            FS-->>Worker: Ok(bytes)
            Worker-->>Caller: Ok(username)
        else 读取失败
            FS-->>Worker: Err(io_error)
            Worker-->>Caller: 通过 ? 立即返回 Err(io_error)
        end
    else 文件不存在
        FS-->>Worker: Err(io_error)
        Worker-->>Caller: 通过 ? 立即返回 Err(io_error)
    end

语法精简对比#

? 运算符背后的自动类型转换#

? 运算符会在错误值上隐式调用 From::from,自动将具体的底层错误转换为当前函数的返回值错误类型:

graph LR
    Expr["评估表达式 Result&lt;T, E1&gt;"] --> Check{"判断是 Ok 还是 Err?"}
    Check -->|Ok value| Extract["解包出具体值并继续向下执行"]
    Check -->|Err e1| Convert["通过 From::from 将类型转为 E2"]
    Convert --> ReturnEarly["从当前函数提前返回 Err(e2)"]

创建自定义错误类型#

在实际项目中,一个函数可能会遇到多种不同来源的错误(I/O错误、JSON解析错误、数据库异常等)。推荐使用枚举类型将其进行统一聚合:

ResultOption 的函数式组合子#

Rust 提供了强大的函数式组合子方法来优雅地变换错误:

  • .map(f): 对 Ok(v) 内部值应用函数 f,保持 Err 不变。
  • .and_then(f): 链式调用下一个可能返回 Result 的操作。
  • .unwrap_or(default): 获取内部值,若为 Err 则回退到指定默认值。
  • .unwrap_or_else(f): 惰性计算闭包以生成默认值。
src/main.rs
fn get_port_or_default() -> u16 {
    std::env::var("PORT")
        .ok()
        .and_then(|p| p.parse::<u16>().ok())
        .unwrap_or(8080)
}
rust

总结#

  • 对于无法恢复的致命缺陷与破坏不变性的情况,使用 panic!
  • 对于预料之中的业务失败,使用 Result<T, E> 交由调用方处理。
  • ? 运算符带来零开销抽象的优雅错误传播与自动类型转换。
  • 自定义 Enum 错误能够以强类型、可扩展的方式统一管理项目错误。

参考资料#