跳至内容

错误处理

这篇讲 Rust 的错误处理哲学:可恢复错误用 Result<T, E>,不可恢复错误用 panic!。没有异常、没有 try-catch、也没有 Go 风格的 if err != nil——Rust 用类型系统让错误处理变得显式且无法忽略。

两种错误:panic vs Result

场景方式示例
可恢复的Result<T, E>文件未找到、网络超时、输入格式错误
不可恢复的panic!数组越界、断言失败、内部不可达状态

panic!——不可恢复的错误

程序进入不可恢复状态时调用 panic!——默认会打印错误信息、展开调用栈并退出:

fn main() {
    // panic!("crash and burn");

    let v = vec![1, 2, 3];
    v[99];  // 索引越界——自动 panic!
}

可以设置 RUST_BACKTRACE=1 环境变量获得详细的调用栈。生产环境下可以配置为直接 abort 而非展开栈(在 Cargo.toml[profile.release] panic = 'abort')。

Result<T, E>——可恢复的错误

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("创建文件失败: {:?}", e),
            },
            other_error => panic!("打开文件失败: {:?}", other_error),
        },
    };
}

便捷方法

上面的代码嵌套太深。Rust 提供了更简洁的方式:

use std::fs::File;

fn main() {
    // unwrap:成功返回值,失败则 panic
    let f1 = File::open("hello.txt").unwrap();

    // expect:与 unwrap 类似,但能提供自定义的 panic 消息
    let f2 = File::open("hello.txt")
        .expect("hello.txt 应该存在");
}

? 运算符

? 是 Rust 错误处理最强大的语法:如果 Ok 则解包,如果 Err立即从当前函数返回错误

use std::fs::{self, File};
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;  // 如果失败,直接返回 Err
    let mut s = String::new();
    f.read_to_string(&mut s)?;             // 同上
    Ok(s)
}

// 更简洁的链式写法
fn read_username_from_file_v2() -> Result<String, io::Error> {
    let mut s = String::new();
    File::open("hello.txt")?.read_to_string(&mut s)?;
    Ok(s)
}

// 终极简短版——标准库已经为你做好了
fn read_username_from_file_v3() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}
? 运算符在遇到不同错误类型时会尝试 .into() 转换。只要错误类型实现了 From<T> trait,? 就能自动转换——这也是 anyhow 这个 crate 特别方便的原因。

? 也可用于 Option<T>

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

? 遇到 None 时立即返回 None,配合链式调用非常流畅。

自定义错误类型

use std::fmt;

#[derive(Debug)]
enum MyError {
    NotFound(String),
    PermissionDenied(String),
    InvalidInput(String),
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            MyError::NotFound(msg) => write!(f, "未找到: {msg}"),
            MyError::PermissionDenied(msg) => write!(f, "权限不足: {msg}"),
            MyError::InvalidInput(msg) => write!(f, "非法输入: {msg}"),
        }
    }
}
实际项目中推荐用 thiserror 宏简化自定义错误类型的定义,anyhow crate 用于应用程序层的快速错误传递——它们生态完善、文档充分,比手写 impl Display 方便得多。

对比:Rust vs Go 错误处理

// Go: if err != nil 地狱
f, err := os.Open("file.txt")
if err != nil {
    return err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
    return err
}
// Rust: ? 运算符让错误无声传播
use std::fs;
fn read_file() -> Result<String, std::io::Error> {
    fs::read_to_string("file.txt")
}

两者都是显式错误处理,但 Rust 的 ? 消除了样板代码,Result 的类型系统保证你不会忘记检查。

一句话小结

Result<T, E> 处理可恢复错误,panic! 处理不可恢复错误。? 运算符是 Rust 错误处理的核心——错误自动向上传播,代码简洁清晰。unwrap()/expect() 适合快速原型和测试。下一篇讲 模块与包管理

练习

  1. 实现一个函数 safe_divide(a: f64, b: f64) -> Result<f64, String>:b 为 0 时返回 Err("除数不能为零".to_string())
  2. ? 改写下面代码:
fn read_config() -> Result<String, std::io::Error> {
    let f = File::open("config.toml");
    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };
    let mut s = String::new();
    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}
参考答案
use std::fs::File;
use std::io::Read;

fn safe_divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("除数不能为零"))
    } else {
        Ok(a / b)
    }
}

fn read_config() -> Result<String, std::io::Error> {
    let mut f = File::open("config.toml")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

fn main() {
    match safe_divide(10.0, 3.0) {
        Ok(v) => println!("10/3 = {v}"),
        Err(e) => println!("错误: {e}"),
    }
}
最后更新于