调试最大的误区
很多人遇到 bug 的第一反应是「改一行试试」。这不是调试,这是抽奖。
抽奖式修改有三个问题:改对了不知道为什么对,改错了浪费一次机会,还可能引入新问题。
真正的调试是一套受控的排除过程:每一次动作都在缩小可能性范围。
六个步骤
1. 稳定复现 → 先能百分百重现,否则无从验证
2. 观察现象 → 记录完整的错误信息,不要只看最后一行
3. 缩小范围 → 二分法定位到具体文件、函数、行
4. 形成假设 → 说出「我认为是因为 X 导致 Y」
5. 验证假设 → 设计一个能证伪它的实验
6. 修复并回归 → 修完确认原问题消失且没引入新问题
跳过任何一步都会让调试变成猜谜。其中最常被跳过、也最不该跳过的是第 4 步。
第一步:先能复现
不能复现的 bug 不应该动手改。因为改完之后你无法确认它是否真的被修复了。
复现时要尽量收窄条件:
- 是必现还是偶现?偶现的话频率是多少?
- 只在某个浏览器 / 某个账号 / 某条数据下出现吗?
- 什么时候开始的?上一次正常是什么时候?
「什么时候开始的」这一问价值极高。 如果知道上次正常的时间点,直接去看那之后的提交,范围立刻缩小几个数量级。
git log --oneline --since="2026-09-20"
第二步:完整读错误信息
新手最常犯的错是只看最后一行。实际上堆栈的价值在于调用链:
TypeError: Cannot read properties of undefined (reading id)
at buildQuery (article.service.ts:42) ← 出错点
at findList (article.service.ts:18) ← 谁调的
at ArticleController.list (controller:31) ← 入口
从上往下读,找出第一个属于你自己代码的帧,那里才是问题的位置。中间的框架代码通常只是传话。
第三步:二分法缩小范围
范围大的时候,二分法是最快的。三种用法:
| 场景 | 二分方式 |
|---|---|
| 不知道哪段代码有问题 | 在中间打日志,看执行到哪一步开始不对 |
| 不知道哪次提交引入的 | git bisect 自动二分查找 |
| 不知道哪条数据有问题 | 把数据分成两半,分别测试 |
git bisect 值得单独提一下,它是自动化的:
git bisect start
git bisect bad # 当前版本有问题
git bisect good v1.2.0 # 这个版本是好的
# Git 会自动切到中间版本,你测试后标记 good 或 bad,重复几次即可定位
第四步:把假设说出来
这一步看起来多余,其实最关键。**「我认为是因为 X 导致 Y」**这句话迫使你给出一个可验证的命题。
对比两种表述:
- 差:「这里好像有问题」→ 无法验证,只能乱改
- 好:「缓存没失效,导致列表返回了旧数据」→ 可以设计实验验证
好的假设通常包含具体的机制。如果说不清机制,说明还没理解足够,应该继续观察。
第五步:验证,而不是「改一下看看」
验证假设的方式是设计一个能证伪它的实验:
- 如果假设是缓存问题 → 手动清掉缓存,看问题是否消失
- 如果假设是并发问题 → 改成串行执行,看问题是否消失
- 如果假设是某条数据异常 → 换一条正常数据,看是否正常
注意:现象消失只说明假设可能对,还需要反过来确认。比如清了缓存问题消失,也可能是因为重启顺带修好了别的东西。所以最好再让问题复现一次,确认可控。
日志还是断点
| 场景 | 用什么 | 原因 |
|---|---|---|
| 本地开发、逻辑复杂 | 断点 | 能逐步查看全部变量 |
| 线上环境 | 日志 | 不能暂停服务 |
| 并发 / 时序问题 | 日志 | 断点会改变时序,把问题藏起来 |
| 循环里大量数据 | 日志 | 断点要按几千次 |
时序问题特别值得强调:断点会暂停线程,从而掩盖竞态条件。这类 bug 必须靠日志,而且日志要带上时间戳和线程标识。
常见认知陷阱
| 陷阱 | 真相 |
|---|---|
| 「这段代码不可能有问题」 | 最可疑的就是这句话指向的地方 |
| 「一定是框架的 bug」 | 大概率是自己用错了,先查文档 |
| 「刚才还好好的」 | 环境或数据变了,去查这两处 |
| 「加了日志就好了」 | 说明是时序问题,日志改变了执行速度 |
最后一条叫海森堡 bug,很常见。遇到时不要高兴得太早,反而应该往并发和时序方向查。
修复之后
修完必须做两件事:
- 回归验证:原场景正常,且相邻功能没被影响
- 留下记录:如果是踩过的坑,加一条注释或一个测试用例
一个 bug 只修一次是浪费。把它变成一条测试用例,才能保证它不会第二次出现。
小结
调试的本质是科学方法在代码上的应用:观察、假设、实验、结论。
流程看起来慢,但它避免了「改十次才碰对一次」的随机性。稳定复现加二分定位,能解决绝大多数问题。