Rust 错误处理的黄金搭档:一个定义错误,一个传播错误

Rust 错误处理的黄金搭档:一个定义错误,一个传播错误

在 Rust 开发中,错误处理是不可或缺的核心环节,但手动实现错误相关 trait 往往会产生大量的样板代码。而为了解决这些样板代码,那就不得不提到两个主流的 Rust 错误处理库 thiserroranyhow 了,现在,我们一起来看看它们解决了什么样的问题。

简化自定义错误

在其他语言中,自定义错误非常简单,以 Go 为例,只需要实现 error 接口即可:

go 复制代码
import "fmt"

type NotFoundError struct {
    Resource string
}

func (e NotFoundError) Error() string {
    return fmt.Sprintf("%s not found", e.Resource)
}

而在 Rust 中,自定义错误就比较麻烦了,需要手动实现 Error、Display 和 Debug 这三个特征。如果需要支持错误自动转换,即适配 ? 操作符,还需要额外实现 From 特征:

rust 复制代码
use std::error::Error;
use std::{fmt, io, num::ParseIntError};

#[derive(Debug)]
enum MyError {
    Io(io::Error),
    Parse(ParseIntError),
    Custom(String),
}

// 实现 Display 特征
impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            MyError::Io(e) => write!(f, "IO 错误: {}", e),
            MyError::Parse(e) => write!(f, "解析错误: {}", e),
            MyError::Custom(s) => write!(f, "自定义错误: {}", s),
        }
    }
}

// 实现 Error 特征,标记该类型为错误类型
impl Error for MyError {}

// 实现 From 特征,支持错误自动转换
impl From<io::Error> for MyError {
    fn from(e: io::Error) -> Self {
        MyError::Io(e)
    }
}

impl From<ParseIntError> for MyError {
    fn from(e: ParseIntError) -> Self {
        MyError::Parse(e)
    }
}

可以看到,仅是定义一个简单的自定义错误枚举,就需要编写大量的样板代码。而 thiserror 库则通过宏解决了这一问题。它能在编译期自动生成上述所有样板代码,让我们专注于错误本身的定义。

我们用 thiserror 来改写上面的示例,实现如下:

rust 复制代码
use std::{io, num::ParseIntError};
use thiserror::Error;

#[derive(Error, Debug)] 
enum MyError {
    // 定义 Display 输出格式,{0} 引用变体第一个字段
    #[error("IO 错误: {0}")]
    // #[from] 自动实现 From 特征
    Io(#[from] io::Error),

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

    #[error("自定义错误: {0}")]
    Custom(String),
}

需要注意的是,thiserror 是基于宏实现,所有代码生成都在编译期完成,运行时零开销,不会对程序性能带来影响。因此,在需要自定义错误类型的场景中,可以大胆的使用 thiserror。

简化通用错误处理

在实际开发中,我们常常会遇到函数返回多种不同错误类型的场景。此时有两种处理思路:一是定义全局自定义错误枚举,也就是上一章节的方案,可以通过 thiserror 简化。二是使用 Box<dyn Error> 作为错误返回类型,无需单独定义错误枚举。

rust 复制代码
use std::error::Error;
use std::fs;

fn read_and_parse(path: &str) -> Result<i32, Box<dyn Error>> {
    let content = fs::read_to_string(path)?;
    let num: i32 = content.trim().parse()?;
    Ok(num)
}

先简单了解下 Box<dyn Error>,由于所有的错误类型都实现 Error 特征,dyn Error 作为动态派发的特征对象,所以可以兼容所有错误类型。

然而由于 dyn Error 是不定长类型(DST),无法直接存储在栈上,因此需要用 Box<T> 智能指针将其分配到堆上。

最后是标准库中已为所有实现 Error 特征的类型实现了 From 转换,因此可以直接使用 ? 操作符自动转换错误类型。标准库实现大致如下:

rust 复制代码
impl<E: Error + 'static> From<E> for Box<dyn Error> {
    fn from(err: E) -> Self {
        Box::new(err)
    }
}

搞懂了 Box<dyn Error> 后,现在我们就可以讲 anyhow 了,anyhow 的底层实现是:

rust 复制代码
Box<dyn Error + Send + Sync + 'static>

不难看出,anyhow 是 Box<dyn Error> 的增强版,具体如下:

简洁的类型别名

anyhow 提供 anyhow::Result<T> 类型别名,简化代码:

rust 复制代码
use anyhow::Result;

fn main() -> Result<()> {
    Ok(())
}

携带业务上下文

anyhow 内置的 context 和 with_context 方法,它们为错误添加上下文:

rust 复制代码
use anyhow::{Context, Result};
use std::fs;

fn read_and_parse(path: &str) -> Result<i32> {
    let content = fs::read_to_string(path)?;
    let num: i32 = content.trim().parse()?;
    Ok(num)
}

fn main() -> anyhow::Result<()> {
    let content = fs::read_to_string("config.json")
        .context("读取配置文件失败")?;
    
    println!("{}", content);
    
    Ok(())
}

// Output:
// Error: 读取配置文件失败

// Caused by:
//     No such file or directory (os error 2)

支持多线程与异步

Box<dyn Error> 未约束 Send + Sync,所以在多线程或异步场景下时会直接报错。而 anyhow 强制约束了 Send + Sync,开箱即用,完全兼容多线程与异步场景:

rust 复制代码
use std::thread;

fn do_work() -> anyhow::Result<()> {
    Err(anyhow::anyhow!("something wrong"))
}

fn main() {
    let handle = thread::spawn(|| do_work());

    let result = handle.join().unwrap();

    match result {
        Ok(_) => println!("success"),
        Err(e) => println!("error: {:#}", e),
    }
}

// Output:
// error: something wrong

结语

在这篇文章中,我们介绍了 thiserror 和 anyhow 的使用方法,同时讲解了它们的设计初衷,解决 Rust 原生错误处理的哪些痛点。学习技术时,知其然更要知其所以然,理解它们为什么存在,才能在实际开发中灵活运用,选择最适合的方案。

推荐阅读

如果这篇文章对你有帮助,欢迎点赞、转发、收藏!

👇 关注我,紧跟前沿技术,少走弯路!

🚀 我正在运营的一个交流群,欢迎加入一起交流,关注公众号后回复【加群】即可获取到加群方法


相关推荐
Moment1 小时前
从多人编辑到 Agent 写文档,Hocuspocus v4 正在改写协同系统 😍😍😍
前端·后端·面试
贺国亚1 小时前
评估-Eval-Hallucination与质量度量
后端·面试
Java内核笔记1 小时前
Spring Security 源码解析(五)表单登录认证全流程:UsernamePasswordAuthenticationFilter 拆解
后端
Dilee2 小时前
Spring AI 对话记忆:MessageChatMemoryAdvisor 最小接入
后端
游码峰行2 小时前
游戏脚本挂攻防-在PoW中实现动态Hash策略及应用实践
后端
一条泥憨鱼2 小时前
苍穹外卖【day6|微信登录与商品浏览功能】
后端·mybatis·苍穹外卖
用户762352425912 小时前
Kafka客户端消息流转流程
后端
橘子星2 小时前
深入理解 AJAX 中的 JSON 序列化与 JS 异步处理
前端·javascript·后端
SimonKing2 小时前
Qoder 提供免费 Qwen3.7-Max,无需订阅
java·后端·程序员