强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Rust 系统编程语言完全教程 / 第09章:错误处理

第09章:错误处理

9.1 两种错误处理策略

Rust 将错误分为两大类:

类型场景关键字是否可恢复
可恢复错误文件不存在、网络超时Result<T, E>✅ 可以处理
不可恢复错误索引越界、逻辑错误panic!❌ 程序终止

9.2 panic! 与不可恢复错误

直接 panic

fn main() {
    // 直接调用 panic!
    // panic!("发生了严重错误!");

    // 数组越界会导致 panic
    let v = vec![1, 2, 3];
    println!("{}", v[99]); // panic: index out of bounds
}

获取回溯信息

# 设置环境变量获取完整回溯
RUST_BACKTRACE=1 cargo run

# 或使用 full 模式
RUST_BACKTRACE=full cargo run

何时使用 panic

场景是否 panic说明
代码中有 bug逻辑错误,如索引越界
用户输入错误应返回 Result
文件不存在应返回 Result
网络超时应返回 Result
初始化失败✅ 可选如果程序无法运行
不可达代码unreachable!()

9.3 Result 与可恢复错误

基本用法

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

fn main() {
    // File::open 返回 Result<File, io::Error>
    let result = File::open("hello.txt");

    let file = match result {
        Ok(file) => {
            println!("文件打开成功");
            file
        }
        Err(error) => match error.kind() {
            io::ErrorKind::NotFound => {
                println!("文件不存在,创建新文件...");
                File::create("hello.txt").expect("创建文件失败")
            }
            io::ErrorKind::PermissionDenied => {
                panic!("没有权限访问文件");
            }
            other => {
                panic!("打开文件失败: {:?}", other);
            }
        },
    };
}

unwrap 与 expect

use std::fs::File;

fn main() {
    // unwrap: Ok 返回值,Err 则 panic
    let f = File::open("test.txt").unwrap();

    // expect: 与 unwrap 相同,但可以自定义 panic 消息
    let f = File::open("test.txt").expect("无法打开 test.txt");
}
方法Ok 时Err 时推荐使用场景
unwrap()返回值panic原型/测试代码
expect("msg")返回值panic(带消息)明确知道不会出错时
unwrap_or(val)返回值返回默认值需要默认值时
unwrap_or_default()返回值类型的默认值类型实现了 Default
unwrap_or_else(f)返回值调用闭包默认值计算昂贵时
unwrap_or_default()返回值默认值需要默认值

? 操作符(错误传播)

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

// 使用 ? 操作符传播错误
fn read_file_contents(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

// 等价的 match 写法
fn read_file_contents_verbose(path: &str) -> Result<String, io::Error> {
    let mut file = match File::open(path) {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut contents = String::new();
    match file.read_to_string(&mut contents) {
        Ok(_) => Ok(contents),
        Err(e) => Err(e),
    }
}

fn main() {
    match read_file_contents("test.txt") {
        Ok(contents) => println!("文件内容:\n{}", contents),
        Err(e) => println!("读取失败: {}", e),
    }
}

链式 ? 操作

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

fn read_first_line(path: &str) -> Result<String, io::Error> {
    let mut contents = String::new();
    File::open(path)?.read_to_string(&mut contents)?;

    Ok(contents
        .lines()
        .next()
        .unwrap_or("")
        .to_string())
}

fn main() {
    match read_first_line("test.txt") {
        Ok(line) => println!("第一行: {}", line),
        Err(e) => println!("错误: {}", e),
    }
}

注意: ? 操作符只能在返回 Result(或 Option)的函数中使用。main 函数也可以返回 Result

main 函数返回 Result

use std::fs;
use std::io;

fn main() -> Result<(), io::Error> {
    let contents = fs::read_to_string("config.txt")?;
    println!("配置内容:\n{}", contents);
    Ok(())
}

9.4 自定义错误类型

基本自定义错误

use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
enum AppError {
    NotFound(String),
    ParseError(ParseIntError),
    ValidationError { field: String, message: String },
    IoError(std::io::Error),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::NotFound(item) => write!(f, "未找到: {}", item),
            AppError::ParseError(e) => write!(f, "解析错误: {}", e),
            AppError::ValidationError { field, message } => {
                write!(f, "验证失败 - {}: {}", field, message)
            }
            AppError::IoError(e) => write!(f, "IO 错误: {}", e),
        }
    }
}

// 实现 From trait 以支持 ? 自动转换
impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self {
        AppError::ParseError(e)
    }
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self {
        AppError::IoError(e)
    }
}

fn parse_age(input: &str) -> Result<u32, AppError> {
    let age: u32 = input.parse()?; // 自动调用 From<ParseIntError>

    if age > 150 {
        Err(AppError::ValidationError {
            field: "age".to_string(),
            message: "年龄不能超过150岁".to_string(),
        })
    } else {
        Ok(age)
    }
}

fn main() {
    for input in ["25", "abc", "200"] {
        match parse_age(input) {
            Ok(age) => println!("年龄: {}", age),
            Err(e) => println!("错误: {}", e),
        }
    }
}

使用 thiserror 简化

# Cargo.toml
[dependencies]
thiserror = "1"
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("未找到: {0}")]
    NotFound(String),

    #[error("解析错误: {0}")]
    ParseError(#[from] std::num::ParseIntError),

    #[error("验证失败 - {field}: {message}")]
    ValidationError { field: String, message: String },

    #[error("IO 错误: {0}")]
    IoError(#[from] std::io::Error),
}

fn parse_age(input: &str) -> Result<u32, AppError> {
    let age: u32 = input.parse()?;

    if age > 150 {
        Err(AppError::ValidationError {
            field: "age".to_string(),
            message: "年龄不能超过150岁".to_string(),
        })
    } else {
        Ok(age)
    }
}

fn main() {
    for input in ["25", "abc", "200"] {
        match parse_age(input) {
            Ok(age) => println!("年龄: {}", age),
            Err(e) => println!("错误: {}", e),
        }
    }
}

9.5 anyhow crate

适用于应用程序级别的错误处理:

# Cargo.toml
[dependencies]
anyhow = "1"
use anyhow::{Context, Result, bail, ensure};
use std::fs;

fn read_config() -> Result<String> {
    let contents = fs::read_to_string("config.toml")
        .context("无法读取配置文件 config.toml")?;

    ensure!(!contents.is_empty(), "配置文件为空");

    Ok(contents)
}

fn process_config(config: &str) -> Result<u32> {
    let port: u32 = config
        .trim()
        .parse()
        .context("配置文件中的端口号无效")?;

    if port == 0 {
        bail!("端口号不能为0");
    }

    Ok(port)
}

fn main() -> Result<()> {
    match read_config() {
        Ok(config) => {
            let port = process_config(&config)?;
            println!("端口: {}", port);
        }
        Err(e) => {
            // anyhow 的错误包含完整的原因链
            println!("错误: {:#}", e);
        }
    }
    Ok(())
}

thiserror vs anyhow

特性thiserroranyhow
定义错误类型✅ 适合❌ 不适合
库代码✅ 推荐❌ 不推荐
应用代码可以✅ 推荐
错误转换手动实现/derive自动(Box<dyn Error>
错误链需手动.context()
类型匹配✅ 可以 match❌ 难以 match

建议: 库使用 thiserror 定义错误类型,应用使用 anyhow 处理错误。


9.6 错误处理最佳实践

分层错误处理

use std::fmt;

// 1. 定义库级别的错误类型
#[derive(Debug)]
enum DatabaseError {
    ConnectionFailed(String),
    QueryFailed(String),
    NotFound,
}

impl fmt::Display for DatabaseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DatabaseError::ConnectionFailed(s) => write!(f, "连接失败: {}", s),
            DatabaseError::QueryFailed(s) => write!(f, "查询失败: {}", s),
            DatabaseError::NotFound => write!(f, "记录未找到"),
        }
    }
}

impl std::error::Error for DatabaseError {}

// 2. 服务层使用库错误
#[derive(Debug)]
enum ServiceError {
    Database(DatabaseError),
    Validation(String),
    Unauthorized,
}

impl fmt::Display for ServiceError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ServiceError::Database(e) => write!(f, "数据库错误: {}", e),
            ServiceError::Validation(s) => write!(f, "验证错误: {}", s),
            ServiceError::Unauthorized => write!(f, "未授权"),
        }
    }
}

impl From<DatabaseError> for ServiceError {
    fn from(e: DatabaseError) -> Self {
        ServiceError::Database(e)
    }
}

// 3. 业务逻辑
fn find_user(id: u32) -> Result<String, ServiceError> {
    if id == 0 {
        return Err(ServiceError::Validation("无效的用户ID".to_string()));
    }

    if id > 100 {
        return Err(DatabaseError::NotFound.into());
    }

    Ok(format!("用户{}", id))
}

fn main() {
    for id in [0, 1, 200] {
        match find_user(id) {
            Ok(user) => println!("找到: {}", user),
            Err(e) => println!("错误: {}", e),
        }
    }
}

9.7 业务场景示例

配置文件加载器

use std::collections::HashMap;
use std::fs;
use std::io;
use std::num::ParseIntError;

#[derive(Debug)]
enum ConfigError {
    Io(io::Error),
    Parse {
        line: usize,
        message: String,
    },
    MissingField(String),
}

impl std::fmt::Display for ConfigError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ConfigError::Io(e) => write!(f, "IO 错误: {}", e),
            ConfigError::Parse { line, message } => {
                write!(f, "第{}行解析错误: {}", line, message)
            }
            ConfigError::MissingField(field) => write!(f, "缺少必要字段: {}", field),
        }
    }
}

impl From<io::Error> for ConfigError {
    fn from(e: io::Error) -> Self {
        ConfigError::Io(e)
    }
}

struct Config {
    data: HashMap<String, String>,
}

impl Config {
    fn from_file(path: &str) -> Result<Self, ConfigError> {
        let contents = fs::read_to_string(path)?;
        let mut data = HashMap::new();

        for (line_num, line) in contents.lines().enumerate() {
            let line = line.trim();
            if line.is_empty() || line.starts_with('#') {
                continue;
            }

            let parts: Vec<&str> = line.splitn(2, '=').collect();
            if parts.len() != 2 {
                return Err(ConfigError::Parse {
                    line: line_num + 1,
                    message: format!("无法解析行: {}", line),
                });
            }

            data.insert(
                parts[0].trim().to_string(),
                parts[1].trim().to_string(),
            );
        }

        Ok(Config { data })
    }

    fn get(&self, key: &str) -> Option<&String> {
        self.data.get(key)
    }

    fn get_required(&self, key: &str) -> Result<&String, ConfigError> {
        self.data
            .get(key)
            .ok_or_else(|| ConfigError::MissingField(key.to_string()))
    }

    fn get_or_default<'a>(&'a self, key: &'a str, default: &'a str) -> String {
        self.data
            .get(key)
            .cloned()
            .unwrap_or_else(|| default.to_string())
    }
}

fn main() {
    // 模拟:创建配置文件
    let _ = fs::write("app.conf", "# 应用配置\nport=8080\nhost=localhost\ndebug=true\n");

    match Config::from_file("app.conf") {
        Ok(config) => {
            let port = config.get_or_default("port", "3000");
            let host = config.get_or_default("host", "127.0.0.1");
            println!("服务器: {}:{}", host, port);

            match config.get_required("debug") {
                Ok(v) => println!("调试模式: {}", v),
                Err(e) => println!("{}", e),
            }
        }
        Err(e) => eprintln!("配置加载失败: {}", e),
    }

    // 清理
    let _ = fs::remove_file("app.conf");
}

9.8 本章小结

要点说明
panic不可恢复错误,程序终止
Result可恢复错误,必须处理
unwrap/expect快捷方式,失败时 panic
? 操作符传播错误,简化代码
自定义错误实现 Display 和 Error trait
thiserror简化自定义错误定义
anyhow应用级错误处理,带错误链
分层处理库定义错误类型,应用统一处理

扩展阅读

  1. Rust Book - 错误处理 — 官方教程
  2. thiserror 文档 — derive 宏文档
  3. anyhow 文档 — 应用级错误处理
  4. Rust 错误处理最佳实践 — Andrew Gallant 博文