错误处理
这篇讲 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() 适合快速原型和测试。下一篇讲 模块与包管理。
练习
- 实现一个函数
safe_divide(a: f64, b: f64) -> Result<f64, String>:b 为 0 时返回Err("除数不能为零".to_string())。 - 用
?改写下面代码:
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}"),
}
}最后更新于