Code Review 经常被误解为”找错”。如果只是找错,那 lint 工具、类型检查和测试套件做得比人好得多。人做 Code Review 的价值在别处:传递知识、建立共识、维护代码库的一致性,以及在变更进入主分支之前捕捉那些工具无法发现的问题——设计取舍、业务逻辑正确性、与系统其他部分的兼容性、对未来维护的影响。

这篇文章从两个角色讲 Code Review:Reviewer(审查者)和 Author(提交者)。两个角色都需要技能,缺哪个都会让 Review 变成摩擦而不是协作。

1. Reviewer:如何给出有效反馈

分级你的评论

不是所有评论都一样重要。把你的评论分级,让 Author 知道哪些必须解决、哪些可以商量、哪些只是挑剔:

级别含义示例
Must / Blocking必须修改,否则不批准逻辑错误、安全漏洞、会破坏现有行为的变更
Should强烈建议修改明显的设计问题、缺少必要测试、可读性严重受损
Nit挑剔,Author 自行决定命名微调、格式细节、风格偏好
Question我不理解,请解释不是问题,只是需要上下文

在评论里明确标注级别,比如 [Must][Nit][Question]。这让 Author 不用猜”这个评论如果不改,会不会阻碍合并”——降低沟通成本,加速 Review 循环。

按优先级审查

人的注意力有限,Review 要有重点。按优先级从高到低:

正确性:代码在所有情况下是否按预期工作?有没有边界情况没覆盖?错误处理是否完整?这是最重要的,也是工具最难检测的。

设计:这个变更是否适合系统的架构?新加的抽象是否必要?会不会带来过度设计,或者相反,有没有把本该分离的东西耦合在一起?

可读性:六个月后的自己,或者一个不了解这段代码背景的人,能读懂吗?变量名和函数名是否准确表达了意图?

格式与风格:代码是否符合项目规范?

注意这个顺序。花大量时间讨论命名和格式,但对逻辑错误轻描淡写,是 Review 资源的严重错配——而且工具完全可以自动处理格式问题。

语气与措辞

这是 Code Review 里最容易被忽视的部分,也是最影响团队关系的。

用问题代替结论

  • "这里应该用 Map 而不是遍历数组" — 命令口气,传递的信息是”你错了”
  • "这里用 Map 的话查找是 O(1),当数据量大时会有差别,不知道当前场景的数据量是否需要考虑这个?" — 提供信息,让 Author 参与判断

说明”为什么”,不只是”什么”

  • "别用 parseInt,用 Number()" — Author 不知道原因,下次还会犯同样的错
  • "parseInt('08') 在某些旧引擎里会被当成八进制解析,用 Number('08') 更可预测" — 传递了知识,不只是指令

赞好的代码:Code Review 不只是挑毛病。如果你看到一个优雅的解法、一个周到的错误处理、一个清晰的测试用例,说出来。这不是客套,而是传递了什么是好代码的信号,也让 Review 不只是负面体验。

评代码,不评人

  • "你又这样写……" — 有攻击性
  • "这个模式在并发场景下可能有竞态,可以看一下……" — 描述代码行为,不带人身判断

2. Reviewer:读 PR 的顺序

不是随机地从第一个文件读到最后一个文件。建议的阅读顺序:

  1. PR 描述 — 先理解这个 PR 要做什么、为什么要做、如何验证。没有描述的 PR 是最难 Review 的——你必须从代码里反推意图,大量时间浪费在猜
  2. 关联 Issue / 背景 — 如果有关联的 Issue 或设计文档,先读
  3. 测试 — 测试是最好的文档,它说明了代码应该做什么。从测试开始看,能帮你建立对实现的预期
  4. 核心实现 — 带着已经建立的预期去看实现,更容易发现偏差
  5. 其他文件 — 配置、文档、类型定义等

3. Author:什么是好 PR

小而聚焦是最重要的属性。一个 PR 解决一个问题。大 PR 意味着:Review 耗时更长、Reviewer 注意力分散、合并后出问题更难定位。业界经验是 400 行以下的 PR 比 1000 行以上的 PR 得到更仔细的 Review。

如果一个任务需要大量改动,考虑拆分:先提一个 PR 建立数据结构,再提一个 PR 实现逻辑,再提一个 PR 接入 UI。这种方式 Review 效率更高,也更容易回滚单个部分。

写好 PR 描述

  • 做了什么:这个 PR 的变更范围
  • 为什么这样做:动机,尤其是不显然的设计决策的原因
  • 如何验证:你是怎么测试的,Reviewer 可以用什么步骤自己验证
  • Breaking Change:如果有,明确说明影响范围和迁移路径

一个没有描述的 PR 是在把理解负担全部转移给 Reviewer。PR 描述的具体格式、Commit Message 写法、Issue 模板,见 技术写作与沟通

主动解释非显然的决策:如果代码里有某个看起来奇怪、或者绕弯路的地方,不要等 Reviewer 来问,在 PR 描述或代码注释里主动解释:

这里没有用 X 是因为……(具体约束) 这个绕弯路的实现是为了避免……(具体问题)

这不只是让 Review 更快,也是防止”好心改回来”——有人看到奇怪的写法,觉得发现了优化机会,改了反而破坏了某个隐性约束。

4. 处理分歧

Code Review 里的分歧有两种,需要不同的处理方式。

技术问题(正确性、性能、安全):有客观依据可以参考。双方可以一起查文档、跑 benchmark、看测试结果来解决。如果还是有分歧,提供足够的信息让一个没有参与讨论的人也能做出判断,或者在评论里留下问题供后来者参考。

口味问题(命名风格、代码组织、抽象方式):没有客观对错,只有偏好和一致性。这类问题的最佳处理方式是:项目有规范就按规范走;没有规范就由 Author 决定,Reviewer 只能建议(Nit 级别),不能阻塞合并。为口味问题消耗大量 Review 时间是浪费。

如果双方都有强观点且无法在 PR 里解决,升级讨论:不在评论里来回拉锯,拉一个同步的对话(视频或语音),10 分钟往往能解决在评论里磨几天的问题。决策结果和理由记录在 PR 或 Issue 里,让后来者知道为什么。

5. 自动化的边界

能自动检测的,不应该消耗人工 Review 的注意力:

  • 代码格式(Prettier、gofmt、Black)
  • 基础 lint 规则(ESLint、golangci-lint、Ruff)
  • 类型错误
  • 已有测试的回归

这些问题在 PR 打开之前,CI 就应该跑完并标记出来。如果 Reviewer 在评论里指出格式问题,说明 CI 没有正确配置——应该修 CI,而不是靠人工 Review 来兜底。

pre-commit hook 在本地就拦截格式问题,是比 CI 更早的防线。要求贡献者在提交前跑 hook 是很多项目 CONTRIBUTING.md 里写明的要求。

这个原则反过来也成立:工具检测不出来的问题,才是人工 Review 真正有价值的地方。

6. AI 辅助 Review 的边界

AI Code Review 工具(GitHub Copilot PR Review、CodeRabbit、Qodo 等)可以自动分析 PR 并给出评论,覆盖常见的问题模式、安全漏洞和潜在 bug。

AI 擅长的:模式匹配(已知的反模式、危险的 API 调用方式)、代码覆盖盲区的提示、一致性检查。

AI 不擅长的:理解业务逻辑是否正确、判断设计决策是否符合项目方向、读懂代码注释和 PR 描述之外的上下文、评估这个变更对系统全局的影响。

实践原则:把 AI Review 当作一道前置过滤器,帮助 Reviewer 在看代码之前先了解明显问题;但不要用 AI Review 替代人工 Review,尤其是涉及核心业务逻辑和安全的部分。AI 标记的每个问题都需要人来判断是否真的是问题,以及如何处理。

7. 原则

Review 是协作,不是审判:目标是更好的代码,不是证明谁对谁错。

说明”为什么”,不只是”什么”:每一条有价值的评论都应该帮 Author 理解为什么需要改,而不只是告诉他改成什么。

自动化能做的,不要人来做:格式、lint、类型、回归测试都应该在 CI 里解决,人工 Review 的注意力留给机器看不到的问题。

小 PR 是对 Reviewer 的尊重:大 PR 是在说”我的时间比你的更重要”。

口味争论设定上限:Nit 级别的问题不阻塞合并,没有规范的风格问题由 Author 决定。