本节摘要:panic 表示"程序自身缺陷被抓住"——越界、算术溢出(debug 下)、显式断言失败。本节讲 panic 的行为(栈展开与终止)、backtrace 的读法,以及 panic 与 Result 的分界判据。读完你能在设计时正确归类错误。
let v = vec![1, 2, 3]; let x = v[10]; // 越界:panic,打印到 stderr 后退庭 let y: u8 = 257u16 as u8; // 不 panic:显式转换语义由你负责 assert_eq!(2 + 2, 4); // 断言失败即 panic,测试的基础 panic!("证据链断裂,无法继续"); // 显式宣判
panic 后发生什么:默认栈展开,逐层运行析构释放资源,打印调用回溯;也可配置为直接终止(panic = abort),二进制更小、无展开成本,嵌入式常用。回溯用环境变量开启后,从出错点到根因的每一帧都列出来——读法与任何语言的调用栈一致。
| 情况 | 归类 | 处理 |
|---|---|---|
| 文件不存在、网络超时、解析失败 | 外部世界的问题 | Result,调用方决定 |
| 数组越界、不变量被打破、空 Vec 取首 | 程序自己的 bug | panic 或前置检查 |
| 调用方传了非法参数且可预期 | 输入问题 | Result 带 InvalidInput 类错误 |
经验判据一句:如果调用方合理地可能需要恢复,就返回 Result;如果恢复意味着掩盖 bug,就 panic。解析用户输入当然 Result;内部缓存明明刚插入却查不到,panic 更诚实。

panic 一旦发生,第一证物是 backtrace。开关在环境变量上,不必改代码。
fn index_sheet(sheets: &[u8], i: usize) -> u8 { sheets[i] // 越界触发 panic,运行期扣留 } fn main() { std::env::set_var("RUST_BACKTRACE", "1"); let s = [7u8, 9]; println!("{}", index_sheet(&s, 5)); }
thread 'main' panicked at src/main.rs:2:5: index out of bounds: the len is 2 but the index is 5 stack backtrace: 0: rust_begin_unwind 1: core::panicking::panic_fmt 2: demo::index_sheet 3: demo::main note: run with `RUST_BACKTRACE=full` for a verbose backtrace
报错首行给出"什么错、在哪",backtrace 给出"怎么到的"。release 构建里符号可能被裁,需要 debug = true 的 profile 才有完整函数名——勘查配置要先于勘查本身。
panic 默认走 unwind:逐层析构后退出线程,主线程 panic 即进程退出。Cargo.toml 可改为直接 abort:省去 unwind 表、二进制更小、代价是丢失 catch 可能。
| 模式 | 行为 | 体积 | 场景 |
|---|---|---|---|
| unwind(默认) | 逐层析构、可被捕获 | 略大 | 常规应用 |
| abort | 立即终止进程 | 更小 | 嵌入式、追求极简 |
panic = "abort" 的项目里,catch_unwind 也无济于事,第 8 章线程一节讨论的"子线程 panic 不拖垮主进程"的前提也随之消失——配置错误处理策略前先确认这一项。
fn risky(v: &[i32]) -> i32 { v.iter().copied().fold(0, |a, b| a.checked_add(b).expect("累加溢出")) } fn main() { let r = std::panic::catch_unwind(|| risky(&[i32::MAX, 1])); match r { Ok(v) => println!("合计 {}", v), Err(_) => println!("外购批次异常,转人工复核"), // panic 被拦下但进程仍在 } }
catch_unwind 不是 try/catch:它只拦 unwind 型 panic,拦不住 abort,也不该用于常规分支逻辑。正当席位是 FFI 边界(panic 穿越 extern "C" 是未定义行为)与测试框架、任务池这类"隔离他人代码爆炸半径"的宿主程序。业务错误用 Result,是本教程反复立的一条铁律。
unwrap 与 expect 不是禁词,但要有使用判据:unwrap 用于"不可能失败,失败即程序错误"的场合(测试代码、原型、静态不变量);expect 附上说明,把"为什么确信"写进字符串。任何一条可能由外部输入触发的路径都不该出现 unwrap——字符串解析、文件读取、网络收包全在其列。code review 时 unwrap 出现在处理输入的函数里,几乎可以直接判退回。
| 现场特征 | 判定 |
|---|---|
| 数组下标、切片边界 | 程序错误,panic 合理 |
| parse 用户输入失败 | 可预期坏数据,必须 Result |
| 文件不存在 | 环境状态,默认 Result |
| 配置值非法 | 启动期一次性校验,panic 或启动失败皆可 |
第四行常起争议:启动期 fail-fast 换取运行期零检查,是正当取舍,前提是失败发生在进程入口而非任意请求路径。把这张卡贴在 code review 的检查单里,unwrap 与 expect 的每次出现都要对号入座。
用一个十分钟实验把本章两节打通:同一份"读取配置并解析端口"的函数写两版——版本一解析失败就 panic,版本二返回 Result 到 main 统一出口;再准备一份坏配置分别喂给两版。观察差异:panic 版的进程退出信息直指 unwrap 行号,但调用方毫无挽回余地;Result 版的 main 可以在退出前打印配置文件名与行号的上下文。结论落回判词:外部输入走 Result 不是教条,是给运维留勘查现场的契约。