代码审查第一阶段:如何系统性地找到藏在角落里的 Bug
我曾经的代码审查方式是「大致扫一眼,凭直觉觉得没问题就批」。这在小团队、小功能时勉强能过,但随着代码库变大、逻辑变复杂,这种做法开始暴露出一个根本性的问题:直觉审查是随机的,不是系统的——你能发现你熟悉领域里的问题,发现不了你没想到的问题。
真正让我意识到要改变,是连续遇到几个「一眼看上去没问题」的 Bug:一个条件判断方向反了,一个金额计算没用定点数、遇到小数出了精度偏差,一个接口没有幂等保护、用户连点两次就写了两条记录。这些 Bug 单独拿出来都「显而易见」,但在 review 时就是没有触发我的警觉。
原因很简单——我在看代码,不是在扫描风险。两件事表面相似,实质不同。
把检查清单当扫描仪,而不是当 checklist 走流程
系统性找 Bug 的核心思路,是把「我凭感觉觉得有没有问题」换成「我按维度逐项排查」。我把风险分为四层:
逻辑错误(CRITICAL):条件判断反转、短路求值陷阱、类型隐式转换(== vs ===)、空值链未守护、返回值被无声丢弃。这类问题一旦触发,通常是直接出 Bug,没有「部分失效」的余地。
边缘条件(HIGH):空数组、零值与假值混淆、金额精度、并发竞争、时间边界(跨天跨月跨年)、分页首末页。这类问题平时不会触发——数据量少、时间凑巧、用户不点边界——但一旦触发,往往难以复现,也很难定位。
数据一致性(MEDIUM):多表操作缺回滚、缓存与数据库不同步、状态机允许非法跳跃、接口缺幂等保护。这类问题常常不是「报错」,而是「数据悄悄错了」,发现时往往已经积累了一堆脏数据。
防御性编程(LOW):异常被吞、配置缺失时默认值不安全、对外部数据格式的硬编码假设。单个问题危害不大,但它们会让系统在某个意外场景下「无声地崩掉」。
扫完不是把所有「看起来可能有问题」的地方都列出来——那会让 reviewer 陷入噪声,开发者也会对 review 失去信任。每个问题都要能说出具体的触发场景:「当 X 发生时,会导致 Y」。说不出来的,就不报。
上下文比 diff 更重要
只看 diff 是另一个常见陷阱。新增的 10 行代码里没有问题,但它调用的是旁边已有的一个函数——而那个函数的返回值在某种情况下是 null,新代码没有守护。
这就是为什么我养成了「向上向下各读 50 行」的习惯:变更点不是孤立的,它的上下文决定了这段代码实际运行时的环境假设。diff 看到的是「改了什么」,上下文看到的是「这个改动活在什么样的世界里」。
技术栈特定的坑值得单独记
除了通用清单,不同技术栈有自己固定的高发陷阱。
PHP 里 find() 返回 null 后直接访问属性是重灾区;update() 返回 0(无更新行)被业务当作失败处理,导致流程异常中断;事务内发起外部 HTTP 请求,超时撑长事务,最终触发死锁——这些坑踩过一次就会记住,但每次换人来审查就得重新「发现」一遍,除非把它们固化成清单。
JavaScript 的异步组件卸载后回调执行(this 已销毁)、JSON.parse 对非 JSON 字符串的未捕获异常;小程序的 wx.request 回调未检查 statusCode,把 HTTP 4xx/5xx 当作成功处理——同样道理。
把这些高发陷阱整理成技术栈专项清单,是让「第一次踩过的人」能够保护「下一次接手的人」的低成本方式。
一句话心法
找 Bug 的本质是系统性地枚举风险,而不是凭直觉感知异常——把「我觉得没问题」替换成「我按清单扫过了」。