Rust 错误处理分层:库代码别急着打印日志

📅 2026/7/3 1:52:21
Rust 错误处理分层:库代码别急着打印日志
Rust 错误处理分层库代码别急着打印日志一、错误处理不是到处写 println刚写 Rust 项目时我很容易在出错的地方直接println!然后返回一个字符串错误。项目小的时候还能看模块一多就乱了有些错误被打印两次有些错误丢了上下文有些库函数直接决定了用户提示。后来才慢慢理解错误处理应该分层。库代码负责描述错误应用入口负责展示错误。也就是说底层模块应该返回结构化错误让上层决定是否记录日志、是否重试、是否展示给用户。库函数里到处打印日志会让 CLI 输出不可控也不利于测试。记得有次写 CLI 工具调用了一个别人写的 HTTP 库。请求失败时终端同时输出了三行错误库里打印的日志、我封装的日志、还有 main 里的打印。三行说的同一个事但措辞完全不同。用户复制给我看我都不确定到底哪条是自己写的。从那次开始我给自己立了一个规矩库代码不打印入口层统一展示。二、分层模型底层保留原因顶层决定表达flowchart TD A[文件模块] -- D[业务服务] B[网络模块] -- D C[解析模块] -- D D -- E[CLI 入口] E -- F[用户提示] E -- G[调试日志]底层错误应尽量具体例如文件不存在、权限不足、响应格式不合法、配置缺少字段。业务层可以把多个底层错误转换成领域错误例如“加载插件失败”。CLI 入口再根据错误类型决定退出码和提示语。这套分层的好处是可测试。测试库函数时只需要断言返回了某个错误不需要捕获 stdout。用户界面也更统一不会出现一部分模块中文提示、一部分模块英文 panic 的情况。错误信息也是产品体验的一部分。三、代码示例thiserror 给库anyhow 给入口下面是一个常见组合库模块用thiserror定义错误应用入口用anyhow汇总上下文。use thiserror::Error; #[derive(Debug, Error)] pub enum ConfigError { #[error(config file not found: {0})] NotFound(String), #[error(invalid config format: {0})] InvalidFormat(String), } pub fn load_config(path: str) - ResultString, ConfigError { std::fs::read_to_string(path) .map_err(|_| ConfigError::NotFound(path.to_string())) }入口层可以补上下文use anyhow::{Context, Result}; fn main() - Result() { let config load_config(agent.toml) .context(failed to start agent because config loading failed)?; println!({config}); Ok(()) }这样底层错误保留类型上层错误保留场景。用户看到的不是一个孤立的 IO error而是知道程序启动失败和配置有关。生产环境实战经验用thiserror时有个坑#[from]会自动做错误转换。有一次我在 ConfigError 上加了#[from]结果 IO 错误被自动转成了 ConfigError。排查的人看到配置文件错误查了半天文件格式其实是文件不存在。自动转换很方便但会让错误类型变模糊。现在我只在明确因果关系时用#[from]其他情况手动map_err。四、实践边界什么时候 panicpanic!不应该用于可预期错误。用户配置错、文件不存在、网络失败、接口超时这些都应该返回Result。panic!更适合表达程序员错误例如不可能出现的内部状态、测试断言失败或原型阶段暂时没有处理的分支。但也不要把错误处理写得过度复杂。小工具里可以先用anyhow快速串起来等模块稳定后再把核心库错误改成明确枚举。学习 Rust 的过程也是逐步抽象的过程不必第一天就写出大型框架。一个因错误处理不当导致线上问题的小案例之前一个后台服务某个协程里unwrap()了一个None直接 panic。因为JoinHandle没被 awaitpanic 被默默吞掉了。服务表面还在运行但那个模块已经不处理新请求了。等发现时已经有上百条请求被丢弃。从那以后所有 spawn 的 handle 都会在退出前 join任何 panic 都会记录到告警通道。日志方面建议入口层或任务边界记录。库函数只返回错误不主动打印。这样用户开启 verbose 时能看到更多细节默认模式保持干净。CLI 工具最怕失败时刷一屏重复堆栈用户反而不知道该改哪里。五、总结Rust 错误处理可以按层设计库代码描述错误业务层补充语义CLI 入口决定展示和日志。thiserror适合定义明确错误anyhow适合应用入口串联上下文。别急着到处打印日志先把错误边界说清楚。