错误处理:区分缺省值、输入错误与程序缺陷
一个服务需要读取端口配置:没有配置时使用 8080,配置为 "443" 时接受它,配置为 "abc"、"0" 或超出范围时给出错误。这里有两种不同的问题:有没有值,以及这个值能不能使用。把它们混为一谈,容易把输入错误写成崩溃,或用默认值掩盖错误配置。
本节只使用 Rust 标准库,例子可保存为单个文件。它没有实际绑定网络端口,只验证配置解析;端口冲突、权限和网络监听属于另一个可能失败的步骤。
1. Option 表示可能没有值
Option<T> 有 Some(value) 与 None 两种情况,适合表达配置未提供、查找没有结果等正常缺省。它本身不保存失败原因。
// 说明片段:有值时保留原值,无值时使用默认值。
let provided: Option<&str> = None;
let text = provided.unwrap_or("8080");
assert_eq!(text, "8080");
这里的 unwrap_or 提供默认值,不会因为 None 而 panic。是否允许缺省是业务规则,不能把所有缺失都默认视为错误,也不能把所有错误都转换成缺省。
2. Result 表示操作成功或失败
Result<T, E> 的 Ok(value) 保存成功值,Err(error) 保存失败信息。例如,字符串转整数可能失败,需要由调用者决定怎样报告或处理。
// 说明片段:明确处理两个分支,不因用户输入无效而 panic。
let parsed = "abc".parse::<u16>();
match parsed {
Ok(port) => println!("解析到端口:{port}"),
Err(error) => eprintln!("端口不是有效的 u16 整数:{error}"),
}
成功解析为 u16 只说明数值在 0..=65535 范围内。下面的配置约定要求 1..=65535,所以还需要拒绝 0。类型能表达的范围与业务允许的范围不是同一回事。
3. 用 ? 将错误交给调用者
在返回 Result 的普通函数中,? 取得成功值;失败时提前返回错误,必要时通过相应转换将错误类型转成函数的返回类型。它不会自动打印日志、重试或忽略错误。
本例先用 map_err 将解析错误改为稳定的教学提示,再用 ? 返回。为了保持快速入门的范围,错误类型使用 String;如果调用者需要按错误种类重试或定位原因,可改用枚举和 std::error::Error,不要依靠匹配错误文案做业务分支。
4. 完整例子:解析端口并检查九种输入
将以下完整代码保存为 port.rs。只有缺省 None 使用默认值;显式提供空字符串或无效值都返回 Err。
// file: port.rs
fn parse_port(input: Option<&str>) -> Result<u16, String> {
let text = input.unwrap_or("8080").trim();
if text.is_empty() {
return Err("端口为空".to_owned());
}
let port = text
.parse::<u16>()
.map_err(|_| "端口必须是 1..=65535 的整数".to_owned())?;
if port == 0 {
return Err("端口必须是 1..=65535 的整数".to_owned());
}
Ok(port)
}
fn main() {
let invalid = || Err("端口必须是 1..=65535 的整数".to_owned());
let cases: [(Option<&str>, Result<u16, String>); 9] = [
(None, Ok(8080)),
(Some("80"), Ok(80)),
(Some(" 443 "), Ok(443)),
(Some(""), Err("端口为空".to_owned())),
(Some("abc"), invalid()),
(Some("0"), invalid()),
(Some("65535"), Ok(65535)),
(Some("65536"), invalid()),
(Some("-1"), invalid()),
];
for (input, expected) in cases {
let actual = parse_port(input);
assert_eq!(actual, expected);
println!("{input:?} -> {actual:?}");
}
}
本地已准备 Rust 工具链时运行:
rustc --edition 2021 port.rs -o port-demo
./port-demo
Windows 可将输出文件命名为 port-demo.exe,随后运行 ./port-demo.exe。代码不依赖第三方 crate;本机未安装 Rust 时,可将上面的完整文件粘贴到 Rust Playground
,选择 Stable 和 Edition 2021 验证。
预期输出如下。2026-10-14 在 Rust Playground 的 Stable / Edition 2021 远程编译运行通过,九个断言及输出均匹配;服务未返回可核对的编译器版本号,本机未复跑。每行输出前的断言还会核对返回值,不能只以程序打印了九行作为成功标准。
None -> Ok(8080)
Some("80") -> Ok(80)
Some(" 443 ") -> Ok(443)
Some("") -> Err("端口为空")
Some("abc") -> Err("端口必须是 1..=65535 的整数")
Some("0") -> Err("端口必须是 1..=65535 的整数")
Some("65535") -> Ok(65535)
Some("65536") -> Err("端口必须是 1..=65535 的整数")
Some("-1") -> Err("端口必须是 1..=65535 的整数")
5. 什么时候使用 panic!、unwrap 与 expect
unwrap 在 Result::Err 或 Option::None 上会 panic;expect 也会 panic,只是提供额外的错误上下文。它们适合某些明确成立的假设或测试断言,不适合代替对外部配置、文件、网络和用户输入的正常错误处理。
panic! 会触发当前执行线程的 panic 路径;按构建策略可能展开栈或终止进程。它不是函数返回的 Err,也不是整个应用的自动恢复机制。不要为了“没有错误返回”而把所有失败改成 panic。
| 情况 | 本节采用的表达 | 调用者要决定什么 |
|---|---|---|
| 配置未提供 | None,按约定使用默认值 | 缺省是否被允许 |
| 配置合法 | Ok(port) | 继续下一步,网络监听仍可能失败 |
| 配置空、非法或越界 | Err(message) | 报告错误,拒绝启动或让用户重试 |
| 已声明假设被破坏 | 可考虑 panic/断言 | 假设是否成立,失败是否确实应停止当前执行 |
6. 练习与验收
- 给上面的用例加入
Some(" "),期望得到“端口为空”,并将数组长度改成10。这检查的是去除空白之后的输入,而不是只判断原字符串是否为空。 - 将
None的默认值改为3000,同步修改该用例的期望值;Some("0")仍必须返回错误。只有缺省值改变,业务范围不改变。 - 将参数改为必填:把
None转成Err("缺少端口配置")。参考做法是在函数开始使用input.ok_or_else(|| "缺少端口配置".to_owned())?,再对返回字符串trim()。对应的None测试必须改成错误期望,其他八个用例继续通过。
官方参考与阅读衔接
- The Rust Programming Language:Error Handling
- The Rust Programming Language:Recoverable Errors with Result
- The Rust Programming Language:Unrecoverable Errors with panic!
- 标准库 Option 文档
本节在 2026-10-14 按上述官方文档修订。它修复了“错误处理”页面误放集合内容的问题;集合仍见第 7 篇
。下一步阅读第 9 篇:泛型与特性
,了解 Result<T, E> 这类类型参数的用法。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。