Rust 错误处理的黄金搭档:一个定义错误,一个传播错误
文章目录Rust 错误处理的黄金搭档一个定义错误一个传播错误简化自定义错误简化通用错误处理简洁的类型别名携带业务上下文支持多线程与异步结语Rust 错误处理的黄金搭档一个定义错误一个传播错误在 Rust 开发中错误处理是不可或缺的核心环节但手动实现错误相关 trait 往往会产生大量的样板代码。而为了解决这些样板代码那就不得不提到两个主流的 Rust 错误处理库 thiserror 和 anyhow 了现在我们一起来看看它们解决了什么样的问题。简化自定义错误在其他语言中自定义错误非常简单以 Go 为例只需要实现error接口即可importfmttypeNotFoundErrorstruct{Resourcestring}func(e NotFoundError)Error()string{returnfmt.Sprintf(%s not found,e.Resource)}而在 Rust 中自定义错误就比较麻烦了需要手动实现 Error、Display 和 Debug 这三个特征。如果需要支持错误自动转换即适配?操作符还需要额外实现 From 特征usestd::error::Error;usestd::{fmt,io,num::ParseIntError};#[derive(Debug)]enumMyError{Io(io::Error),Parse(ParseIntError),Custom(String),}// 实现 Display 特征implfmt::DisplayforMyError{fnfmt(self,f:mutfmt::Formatter_)-fmt::Result{matchself{MyError::Io(e)write!(f,IO 错误: {},e),MyError::Parse(e)write!(f,解析错误: {},e),MyError::Custom(s)write!(f,自定义错误: {},s),}}}// 实现 Error 特征标记该类型为错误类型implErrorforMyError{}// 实现 From 特征支持错误自动转换implFromio::ErrorforMyError{fnfrom(e:io::Error)-Self{MyError::Io(e)}}implFromParseIntErrorforMyError{fnfrom(e:ParseIntError)-Self{MyError::Parse(e)}}可以看到仅是定义一个简单的自定义错误枚举就需要编写大量的样板代码。而 thiserror 库则通过宏解决了这一问题。它能在编译期自动生成上述所有样板代码让我们专注于错误本身的定义。我们用 thiserror 来改写上面的示例实现如下usestd::{io,num::ParseIntError};usethiserror::Error;#[derive(Error, Debug)]enumMyError{// 定义 Display 输出格式{0} 引用变体第一个字段#[error(IO 错误: {0})]// #[from] 自动实现 From 特征Io(#[from]io::Error),#[error(解析错误: {0})]Parse(#[from]ParseIntError),#[error(自定义错误: {0})]Custom(String),}需要注意的是thiserror 是基于宏实现所有代码生成都在编译期完成运行时零开销不会对程序性能带来影响。因此在需要自定义错误类型的场景中可以大胆的使用 thiserror。简化通用错误处理在实际开发中我们常常会遇到函数返回多种不同错误类型的场景。此时有两种处理思路一是定义全局自定义错误枚举也就是上一章节的方案可以通过 thiserror 简化。二是使用Boxdyn Error作为错误返回类型无需单独定义错误枚举。usestd::error::Error;usestd::fs;fnread_and_parse(path:str)-Resulti32,BoxdynError{letcontentfs::read_to_string(path)?;letnum:i32content.trim().parse()?;Ok(num)}先简单了解下Boxdyn Error由于所有的错误类型都实现 Error 特征dyn Error 作为动态派发的特征对象所以可以兼容所有错误类型。然而由于 dyn Error 是不定长类型DST无法直接存储在栈上因此需要用BoxT智能指针将其分配到堆上。最后是标准库中已为所有实现 Error 特征的类型实现了 From 转换因此可以直接使用?操作符自动转换错误类型。标准库实现大致如下implE:ErrorstaticFromEforBoxdynError{fnfrom(err:E)-Self{Box::new(err)}}搞懂了Boxdyn Error后现在我们就可以讲 anyhow 了anyhow 的底层实现是BoxdynErrorSendSyncstatic不难看出anyhow 是Boxdyn Error的增强版具体如下简洁的类型别名anyhow 提供anyhow::ResultT类型别名简化代码useanyhow::Result;fnmain()-Result(){Ok(())}携带业务上下文anyhow 内置的 context 和 with_context 方法它们为错误添加上下文useanyhow::{Context,Result};usestd::fs;fnread_and_parse(path:str)-Resulti32{letcontentfs::read_to_string(path)?;letnum:i32content.trim().parse()?;Ok(num)}fnmain()-anyhow::Result(){letcontentfs::read_to_string(config.json).context(读取配置文件失败)?;println!({},content);Ok(())}// Output:// Error: 读取配置文件失败// Caused by:// No such file or directory (os error 2)支持多线程与异步Boxdyn Error未约束 Send Sync所以在多线程或异步场景下时会直接报错。而 anyhow 强制约束了 Send Sync开箱即用完全兼容多线程与异步场景usestd::thread;fndo_work()-anyhow::Result(){Err(anyhow::anyhow!(something wrong))}fnmain(){lethandlethread::spawn(||do_work());letresulthandle.join().unwrap();matchresult{Ok(_)println!(success),Err(e)println!(error: {:#},e),}}// Output:// error: something wrong结语在这篇文章中我们介绍了 thiserror 和 anyhow 的使用方法同时讲解了它们的设计初衷解决 Rust 原生错误处理的哪些痛点。学习技术时知其然更要知其所以然理解它们为什么存在才能在实际开发中灵活运用选择最适合的方案。