更严格的 Clippy 配置,帮你在生产环境少踩几个坑
Rust 的编译器能挡掉不少错误,但很多会在生产环境里“爆雷”的问题,它其实也管不住。本文从一个真实的线上事故出发,聊聊为什么你应该把 Clippy 配置得比默认更严格——尤其在 AI 编码代理写代码越来越普遍的当下,一套更严格的静态检查规则,相当于给代码库加了一道重要的安全护栏。
一次生产事故:字符串切片引发的 panic
Scour 是我在做的个性化内容信息流服务。每周五,它都会给每个用户发送一封邮件摘要,汇总本周与他们兴趣最匹配的热门帖子。可就在某个周五,邮件发送任务突然停了下来。
这让我有点意外,因为此前我已经在类型系统层面做了不少防护,也写了测试,确保任务在任何错误发生时都能记下日志并继续跑。
但日志很快告诉了我真正的原因:线程 tokio-runtime-worker 发生了 panic——byte index 200 is not a char boundary(字节索引 200 不是字符边界)。也就是有个函数在截断文章摘要时,没有检查 UTF-8 的字符边界,直接按字节位切片,结果把正在处理邮件任务的 Tokio 工作线程给弄崩了。
这个问题本身修起来不难,换一种尊重 UTF-8 边界的截断方式就好。但它让我联想到 2025 年 Cloudflare 那个“搞崩互联网”的 unwrap bug——都是很基础的失误,却造成了相当大的影响。所以我想找一个更通用的方案,在问题发生之前就把它拦住。
编译通过 ≠ 万事大吉
Rust 编译器确实能挡掉很多 bug,但生产环境里仍然有一些问题是它抓不到的:
panic要么直接让程序崩溃,要么悄悄终止某个工作线程;- 死锁或被丢弃的 future 会让任务无声无息地卡住;
- 大量数值运算可能在毫无提示的情况下产生错误结果。
这些问题,编译器不一定能帮你发现。而把 Clippy 配置得比默认更严格,就能防住不少此类风险。
在编码代理流行的时代,这件事的意义更大了。经验丰富的 Rust 工程师或许能靠直觉避开一些容易出问题的写法,但编码代理或初级同事未必。更严格的 Clippy 规则,可以帮你更放心地依赖别人写的代码。而且,一次性地在现有代码库上启用新 lint,本身也是个繁琐的活儿——正好适合交给编码智能体来干。
启用更多 Clippy 检查
Clippy 自带几百个默认关闭的 lint。有些是因为容易误报才被关掉,有些则是风格上的选择,你可能合理地不想用。那我们该启用哪些检查,才能重新找回“只要编译通过,Clippy 也通过,就能放心运行”的感觉?
为什么不直接启用整个 lint 类别?
Clippy 的 lint 按类别分组,包括:
Correctness(正确性)Suspicious(可疑)Complexity(复杂性)Perf(性能)Style(风格)Pedantic(迂腐)Restriction(限制)CargoNursery(实验性)Deprecated(已弃用)
可惜的是,没有哪个类别能清晰地对应“别让它在生产环境里 panic 或出错”。Clippy 官方文档也明确提醒:Restriction 类别绝对不应该整体启用。Clippy 甚至专门提供了一个 lint(叫 blanket_clippy_restriction_lints),来阻止你一次性启用整个类别。
因为 Restriction 里既有许多有用的检查,也包含不少彼此矛盾的规则。比如,它同时包含强制使用大端字节序和强制使用小端字节序的 lint。文档的建议是:“启用之前应逐个考虑。”你当然可以先整体启用 pedantic 和 restriction,再单独放行某些不想要的项,但更推荐的做法是:逐个挑选,按需启用。
不触发的 lint 也有价值
有些 lint 可能在你当前的代码库里完全不会触发,那是不是就不值得开?我不这么认为。
不触发的 lint 可以当作一条低成本的“绊线”。万一以后有人——不管是同事、你自己,还是某个编码智能体——引入了这种写法,它能立刻提醒你。相比让这类问题悄悄溜进生产环境,提前响一下警报要划算得多。
我的 lint 选择
在分享我的配置之前,先说两个前提:
- 每个项目情况不同,你应该自己翻一翻可用的 lint,挑适合的。
- 如果你的最低支持 Rust 版本(MSRV)早于 1.95,建议查一下这些 lint 的稳定版本是否晚于你的 MSRV,因为其中一些是最近才进入 stable 的。
下面这些 lint 是我常用的,按照它们防范的行为类型分了组。如果你只想直接抄配置,也可以跳到文末。
别崩溃(Don't Panic)
这一组 lint 主要负责防止 unwrap、不安全的切片或数组/字符串索引导致的 panic。
需要说明的是,其中一些 lint(比如 string_slice 和 indexing_slicing)可能会在你的整个代码库里产生大量警告,修起来相当繁琐。但换个角度看,改用 .get() 和迭代器这类更安全的写法,可以避开不少极其隐蔽的坑。我个人觉得,这笔账是算得过来的。
关键要点
- Rust 编译器只能保证类型层面的正确,拦不住运行时 panic、死锁一类的问题。
- Clippy 默认关闭的 lint 里有大量有用的检查,值得按需启用。
- 不要整体启用
Restriction类别,里面有些规则互相矛盾;应该逐个挑选。 - 即使某个 lint 当前“用不上”,它也能作为未来的安全绊线,防止新代码引入隐患。
- 在 AI 编码代理逐步介入日常开发的背景下,严格 lint 配置等于给代码库加了一道可靠护栏。
本部分继续介绍更严格的 Clippy 配置:除了防范 panic 的 lint,还包括静默失败、异步死锁、内存安全、数字运算等几类问题。同时讨论了
expect_used与arithmetic_side_effects这两个 lint 的实际取舍,帮助你在“安全”与“噪音”之间找到平衡。
继续:防范 panic 的 lint
以下这些检查专门拦截可能触发 panic 的代码写法:
string_slice:对&str执行&s[a..b]之类的切片操作。字符串是 UTF-8 编码,如果切片边界落在多字节字符中间,程序会直接 panic。作者最初遇到的那个 bug,正是这类代码导致的。indexing_slicing:arr[i]或&arr[a..b]这类数组索引与切片操作。unwrap_used:调用Option::unwrap/Result::unwrap。panic:调用panic!()宏。todo/unimplemented/unreachable:这些占位性质的 panic 宏。get_unwrap:vec.get(i).unwrap()这种先取Option再unwrap的写法。unwrap_in_result:在返回Result的函数中使用.unwrap()。unchecked_time_subtraction:对Instant做减法时,如果被减数小于减数,会直接 panic。panic_in_result_fn:在返回Result的函数中使用panic!或assert!。
至于 expect_used,要不要启用就看个人取舍了。调用 .expect 的确可能引发 panic,但传给它的消息本来就应该说明“为什么这里不会出问题”。如果启用了这个 lint,你就得在确实不会出错的地方写 #[expect(expect_used, reason = "...")] 逐个局部关闭,而这些 reason 往往和你原本写 .expect 时给出的理由完全一样,等于重复劳动。
另一个值得权衡的是 arithmetic_side_effects。它能拦住溢出和除零错误,代价是 Clippy 会对每一处用到 +、-、*、<<、/、% 等运算符的代码发出警告。作者在自己的代码库里试过开启它,估计约 15% 的警告抓到了真实问题,剩下的 85% 都是噪音。
不要静默失败
这些 lint 用于避免错误被悄悄吞掉:
let_underscore_future:let _ = future会直接丢弃 future,而不等待(await)它执行。let_underscore_must_use:let _ = result_returning()会丢弃必须使用的返回值,从而忽略错误。unused_result_ok:result.ok();会静默丢弃Err值。map_err_ignore:.map_err(|_| MyErr)丢掉了原始错误信息,只留下一个自定义错误。assertions_on_result_states:assert!(r.is_ok())在断言失败时不会输出Err中的具体信息。
不要做糟糕的异步操作
这些问题可能引发并发错误和死锁:
await_holding_lock:持有MutexGuard跨越.await点。await_holding_refcell_ref:持有RefCell::borrow_mut的引用跨越.await。if_let_mutex:if let _ = mutex.lock() { other_lock() }这种死锁模式。注意:只有使用早于 2024 的 Rust 版本时此 lint 才相关,2024 edition 已修复作用域问题。large_futures:future 太大,可能导致栈溢出。
不要对内存做不安全的事情
mem_forget:mem::forget会造成内存泄漏。undocumented_unsafe_blocks:每个unsafe {}块都需要写// SAFETY:注释。multiple_unsafe_ops_per_block:每个块内只应有一个不安全操作,每个操作配一条注释。unnecessary_safety_doc/unnecessary_safety_comment:只在必要的位置写安全性说明。
不要用数字做可能出错的事情
float_cmp:对浮点数使用a == b。float_cmp_const:更严格,还会标记与常量比较的情况。lossy_float_literal:被静默舍入的浮点字面量(例如16_777_217.0_f32)。cast_sign_loss:(-1_i8) as u64会回绕成u64::MAX。invalid_upcast_comparisons:(x: i32 as i64) > i32::MAX as i64永远为 false。
至于 cast_possible_wrap、cast_precision_loss、cast_possible_truncation 这几个 lint,它们会强制你在做可能有损的数字类型转换时,明确记录不变量。对有些人来说很有用,对另一些人可能觉得多余。