1. 为什么“开放”这件事,恰恰是Code Review的灵魂
我在接手团队技术管理之后,有一件事让我特别矛盾——我从入职第一天就强调Code Review,周会也讲、考核也挂钩,但代码质量并没有明显改善。后来我把所有人提的Review意见导出来看了一遍,发现问题根本不在“有没有做CR”,而在于大家的Review都是关起门来凭感觉:今天这个人心情好就多写两句,明天另一个人赶版本就点个“通过”完事。没有统一标准、没有节奏、没有沉淀,Review变成仪式,问题代码照样合入主干。
那阵子我正好在整理团队内部的开源项目,顺手把Code Review这一套流程也开源了出去,名字就叫open-code-review。这个项目本身不复杂,但它把“代码审查”从个人经验变成了团队可复用的资产。今天想围绕这个东西,把我踩过的坑、总结出来的方法、以及最终沉淀出的实操流程一次性聊透。
先解释一下为什么标题里要强调“open”。很多团队把Code Review当成开发流程里的一个“关卡”,只有合代码的时候才打开一下。而open-code-review的思路正好反过来:审查规则是公开的、模板是公开的、常见问题库是公开的、工具链配置也是公开的。任何新的成员加入,不需要反复口传心授,直接看这套东西就知道“我们团队怎么Review代码”。这才是“开放”二字的真正价值——它不是把代码公开,而是把审查这件事本身做成透明的、可复制的标准动作。
这个方案适合谁?如果你是技术团队的负责人,或者开源项目的维护者,又或者你只是一个人写项目但想提前建立起良好的代码审查习惯,那么这篇文章里的流程和模板都可以直接拿来用。后面我讲的每一条,都是我在实际工程里试过、踩过坑、最后确认有效才写下来的。
2. 一次完整的Code Review,流程应该怎么设计
很多团队说“我们做Review”,实际上就是PR挂上去之后等人留言,有时挂三四天没人理,有时五六个Reviewer每人丢一句“LGTM”就合了。这种流程我称之为“被动式Review”,效率低,而且审查质量完全取决于当天谁有空、谁心情好。要让Code Review真正有效,流程必须在提交代码之前就已经设计好。
2.1 变更规模控制:别让Reviewer硬啃一千行
我发现几乎所有撑不过三个月的Review制度,死因都是PR太大。五百行的小重构可能还有人愿意看,一千行以上的功能分支,Reviewer翻开Diff看到十几个文件,绝大多数人第一反应就是“这怎么审”,然后草草看两眼就点了通过。这不是态度问题,是认知负荷超载,人脑一次能处理的上下文就是有限。
open-code-review里把这条定成了硬性规范:单个PR建议不超过400行变更,逻辑上无法拆分的重构除外。超过这个量级的改动,必须提前在PR描述里写清楚“为什么这次变更无法拆小”,并且拆出对应的审阅计划。实操下来,我见过最极端的一次是一个同事为了赶需求提交了1800行的PR,我在下面没有直接给代码意见,而是让他先拆成主题提交:第一笔交接数据模型变更,第二笔核心逻辑,第三笔测试,第四笔文档和配置。拆完以后,每个PR大概三百行上下,四个人轮流审,两天就全部合完了,合入质量反而比猛啃一个大PR要高很多。
这里有个跟直觉相反的经验:拆PR不是把一次变更的时间拉长了,反而是压缩了总时间。因为大PR的Review等待时间和返工成本是指数增长的,拆小了以后并行度上来了,人的注意力也能保持在线。
2.2 检查清单要具体到能打勾
没有清单的Code Review,本质上就是在靠Reviewer临时发挥。每个人的知识结构不同、关注点不同,写Java的人可能对SQL注入无感,写前端的人对一个死锁问题可能完全无感。这种靠个人能力摊大饼式的Review,结果必然是不稳定的。
open-code-review里维护了一份团队通用检查清单,我把它按维度拆成以下六类:
- 功能正确性:这段代码真的解决了PR描述里的问题吗?有没有遗漏分支?核心逻辑有没有覆盖三种以上边界情况?
- 错误处理:失败路径有没有被妥善处理?外部依赖超时、断连、空值、非法参数,每种情况是否有明确回应?
- 性能与并发:这个循环能不能不写进事务里?加锁的范围是不是太大了?这个缓存有没有考虑失效策略?
- 安全隐患:有没有未经验证的输入拼接进SQL或Shell?敏感日志有没有打印token和密钥?文件上传有没有限制类型和大小?
- 可测试性:这段逻辑有没有直接new了一个依赖导致单测无法Mock?是不是所有公共方法都有对应的测试覆盖?
- 可维护性:三个月后的新同事能不能不看设计文档就理解这段代码?变量命名是否表意?函数是不是只做了一件事?
这个清单不是拿来逐条回复的,而是给Reviewer一个内部checklist,让他审查的时候不要只盯着“这段逻辑对不对”就完事了。我见过太多Review意见只停留在“这里少了个分号”“这个变量名不好”的层面,不是说这些不能提,而是如果整个Review只提了这些,说明审得不够深,没有进入逻辑层面和架构层面。
2.3 自动化工具先跑第一层:静态检查与CI
人脑的资源是稀缺品,所以不应该用来处理机器就能发现的问题。open-code-review里我一开始就把自动化检查放在整个人工Review之前——所有PR在创建的同时,CI会跑完编译、单元测试、Lint、静态扫描,有任何一项红掉,Reviewer不需要开始人工审查。一句话:所有人类Reviewer的时间,只花在机器判断不了的事情上。
这个原则听起来理所当然,但实际团队执行起来特别容易走样。最常见的场景是,CI跑了二十分钟挂了,开发在群里喊“帮忙先看一眼,就是测试挂了,代码逻辑没问题”。一开始可能还好,但破窗效应一旦出现,以后大家就默认“红着也可以审”。我后来在open-code-review里加了一条规则:任何状态下CI失败超过两小时且未处理的PR,自动从Review队列移出,优先级降到最低。宁可晚合一会儿,也不要让团队养成“带病审查”的习惯。
3. 工具选型与实现细节:从轻量到重量级
Code Review的工具链非常成熟,不用自己造轮子,但很多人不知道怎么选。open-code-review里我整理了三档方案,分别对应不同规模的团队,你可以按需取用。
3.1 GitHub / GitLab 自带审查功能怎么用到位
如果你的团队代码托管在GitHub或者GitLab上,那最基本的能力已经具备了——PR/MR的Diff查看、行内评论、审查通过/请求修改的状态、多人Approval规则,这些原生功能完全够用。很多人没有用到位的是分支保护规则(Branch Protection)和审批人数策略。
我建议至少这样配置:
- 主干分支禁止直接推送,所有变更必须通过PR/MR合入;
- 至少一个非作者本人的Approval才能合入;
- 过期的Approval在代码更新后自动失效,防止“先approve后改代码”的老油条行为。
第3条是最容易被忽略但最重要的。我印象很深的一次事故是,一个同事在凌晨三点提交了一个PR,自动测试都过了,他给自己点了Approval(作者本人居然有权限approve),然后直接合入,把生产环境搞挂了。原因就是分支保护规则没配好,Allow specified actors to bypass pull request requirements这个选项被错误打开了。GitHub上所有保护规则,建议全部保持默认的严格模式,哪怕是团队Owner也要走审批流程。
3.2 Reviewdog与代码注释机器人
第二档方案是引入自动化审查机器人,这里我强烈推荐Reviewdog。它的核心能力是把Lint工具和静态检查工具的输出,直接以行内评论的形式贴到PR对应代码行上,Reviewer打开PR就能看到“这一行有问题”“那一行有潜在bug”,不用自己对照日志去猜。
open-code-review里给一套Java/Spring技术栈配过一组组合命令,实测下来效果很好。核心配置大致长这样:
name: reviewdog on: pull_request: types: [opened, synchronize] jobs: eslint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: reviewdog/action-eslint@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} reporter: github-pr-review level: warning这里最关键的是reporter: github-pr-review,它表示审查结果以PR review评论的形式出现,而不是简单的commit status提示。还有level: warning,意思是错误级别以下的提示不会以error形式阻塞PR,给开发留出处理空间。真正需要阻断合入的问题,建议升到error级别。
还有一类工具叫“自动分配Reviewer”,比如GitHub的CODEOWNERS文件。这个文件可以指定每个目录、每个模块的负责人,当PR涉及对应路径时,系统会自动把Reviewer指派给负责人。这比每次在群里喊“谁有空看下这个PR”要靠谱得多,责任到人。
3.3 SonarQube等平台与质量门禁
如果团队规模到了二三十人以上,或者你们对代码质量有合规性要求,那么可以考虑引入SonarQube这一类的代码质量平台。它跟Reviewdog的思路不太一样:Reviewdog有点像一个严厉的注释机器人,而SonarQube更像是把代码扫描结果做成了一套看板系统,它的Quality Gate可以设定“新增代码不可引入任何Blocker级别问题”“测试覆盖率不得低于80%”这类硬性门槛。
在open-code-review里我把SonarQube定位成“守住底线”的工具,它不太适合用来做精细的人工审查替代品——因为它的误报率在高版本上虽然降了很多,但依然存在,如果让机器人一报错就锁死合入,开发一定会想方设法绕过它。我见过有人为了过覆盖率门禁,写了一堆只执行不断言的空测试,那比不写测试还恶心。
所以我的建议是:把SonarQube这类平台设置为“报告级”而不是“阻塞级”。也就是扫描结果不看质量分,看固定几个安全高危项和重复率超过阈值的告警,只有这些才挂到PR状态上。剩余的中低级问题,进入月度技术债务清单慢慢还。
| 工具/能力 | 定位 | 适用规模 | 建议策略 |
|---|---|---|---|
| GitHub/GitLab原生审查 | 人工Review的基础设施 | 任何规模 | 强制启用分支保护与审批人数 |
| Reviewdog | 自动行内评论机器人 | 5~50人 | 接入CI,结果进PR评论区 |
| SonarQube | 代码质量看板与扫描平台 | 20人以上或合规要求 | 质量门禁只卡高危项,其余进债务池 |
4. 实操过程与核心环节实现
看完选型以后,接下来就是真刀真枪跑起来。这一章我拿一个真实的PR场景来走一遍完整流程,从提交到合入,看看每个环节具体怎么做。
4.1 用GitHub Actions搭一个自动审查流水线
先来一个可以直接抄作业的GitHub Actions配置。这个workflow会在每次新PR提交时,同时跑三件事:单元测试、Lint、Reviewdog扫描。整个流程控制在五分钟以内。
name: auto-review on: pull_request: types: [opened, synchronize] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 - run: npm ci - run: npm test lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 - run: npm ci - run: npm run lint reviewdog: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: reviewdog/action-eslint@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} reporter: github-pr-review level: warning三个job是并行执行的,理论上总耗时基本等于最慢的那个job。on.pull_request.types这里选了opened和synchronize,意思是新PR创建和代码更新时各触发一次。这里有一个细节:synchronize事件在force push后也会触发,如果是那种一直在force push修改的PR,可能会连续跑很多次CI,建议团队约定不要用force push往已有PR上铺代码。
4.2 人工Reviewer的检查顺序
机器检查全部通过之后,才轮到人工上阵。但人工Review也不是一上来就逐行读代码。我给自己定了一个固定顺序,分享出来:
第一步先看PR描述和任务关联。要是PR描述里只说“fix bugs”三个字,我根本不会点开代码,先让作者把“改了什么、为什么改、怎么验证”写清楚。open-code-review的PR模板里固定有“变更背景”“实现方案”“验证步骤”“风险点”四个段落。没有这四个段落的PR,Reviewer可以直接打回,这是有依据的。
第二步看测试代码。对,不是先看实现代码,而是先看测试。因为测试代码能够最真实地反映作者对自己代码的理解程度。如果测试只覆盖了Happy Path,说明边界情况没想清楚;如果测试覆盖了大量异常场景和边界值,那实现代码大概率是有经过深思的。
第三步才是看实现代码,按主流程走一遍,对照测试验证作者是不是真的在测他写的东西。
最后一步,回到PR整体,看这个变更是否影响了其他模块,有没有同步更新文档、配置、数据库脚本。这一步最容易漏,因为这已经超出“代码”的范畴了,但对上线成功率影响最大。
4.3 异步评审与同步评审的取舍
Review的形式有两种,一种是异步的——也就是PR上留言,大家有空了来看;另一种是同步的——会议室里拉一群人,投屏过代码。open-code-review里我强烈推荐异步评审为主,同步评审只用于特定场景。
为什么?因为同步评审的效率实在太低了。四个人围在一起花一小时过一屏代码,真正有效讨论的时间可能不到十五分钟,其他人都在扮演人形占位符。而且同步评审有个心理问题:当技术负责人或者资深工程师在场时,其他人很容易陷入“大佬都点头了,那我也没意见”的从众状态,整个评审演变成一种表演。
异步Review才是常态化的主力。每个人在自己精力最好的时段打开PR,深思熟虑后给出意见,作者看到后逐条回复。这个过程本身带有一股异步协作的纪录片质感——每个评论都是被认真思考过的,每条回复都清晰可见。而同步评审,我只在三种情况下使用:架构级的大方向决策;跨团队接口联调前的对齐;线上事故后的复盘代码走查。其他时候,别开会,直接在PR上聊。
5. 常见问题与排查技巧实录
代码审查制度推行过程中,一定会遇到各种阻力。很多团队不是没有工具,也不是没有流程,而是制度在执行层面变形了。这一节我把这些年遇到的高频问题集中整理一下,对应的解法都是经过验证的。
5.1 审查意见太多导致互相敷衍
这个问题几乎每个推行过严格CR的团队都会遇到。Reviewer非常认真,每条都提,作者一看四五十条评论当场头皮发麻,回着回着就累了,最后变成“OK, done”,再往后Reviwer也懒得提了,因为提了也没人认真改。
我的解决方案是给意见分级。在open-code-review的文档里,我规定所有Review意见必须带前缀,分为P0、P1、P2三档:
- P0:会导致功能错误、安全隐患、数据丢失的问题,必须修复后才能合入;
- P1:不会导致功能性错误,但会影响可维护性、可测试性、性能的问题,建议合入前修复;
- P2:风格、命名、重构建议等优化项,可以记录为后续优化,不阻塞合入。
这个分级看起来很简单,但它解决了两个很重要的心理问题。第一,作者不再因为一堆P2评论感到被冒犯,他知道真正的关键项是那两三个P0。第二,Reviewer在提意见的时候自己也会思考“这个真值得花作者五分钟去改吗”,提的意见质量立马上来了。
5.2 Reviewer不响应怎么办
有规则但没人执行,同样等于没有。团队里最让人抓狂的不是代码质量差,而是PR挂了一个礼拜没人Review。我见过有团队为这个搞了复杂的积分制度,实际上最简单的办法就是轮值+超时升级。
具体的做法是:每两周排一个Review值班表,每天安排一个人负责当天所有的Review兜底。如果某个PR超过12小时没有拿到任何Review意见,值班者有义务第一时间审完或者协调其他人来审。如果值班者也忙不过来,那就按紧急程度升级到技术负责人。
我在open-code-review里配合了这个规则,还在CI脚本里加了一个定时检测的workflow,每小时扫一次所有打开超过24小时的PR,自动在群里发一条提醒:这条PR已经晾了多久、作者等得多焦虑。不要低估这种自动提醒的价值,它把“没人在意”变成了“系统在盯”,人性就是这样,一个公开的催促远比私聊有效。
5.3 重构PR爆炸式增长怎么拆
先给结论:如果一段旧代码需要彻底推翻重建,不要尝试在PR里做混合式重构。我自己的经验是,拆分重构PR要按“行为保持不变”和“行为发生变化”两个方向切。
第一步,先做一个纯结构迁移的PR,只调整模块位置、类名、函数签名,不改变任何外部行为,用现有的测试来保障正确性。这类PR基本是编译器帮你兜底的,Reviewer审起来轻松,风险低。
第二步,再做行为变更的PR,把新的需求或修复逻辑加进去。到了这一步,因为结构已经整洁了,改起来清楚了很多,Review也能聚焦在真正的业务逻辑上。
最怕的就是有人图省事,把结构迁移和行为变更塞在一个PR里,Reviewer看到一千行Diff,有一半是纯移动代码,另一半是新逻辑,两件事搅在一起,审完头都是大的。如果你发现PR里出现大量的“move code”和“change logic”交错分布,那基本可以判定这个PR需要拆分了。
6. 关于节奏、心态与一点额外的建议
前面讲的都是方法和工具,最后我想聊聊更偏软性、但同样重要的东西。
关于Review节奏,我的体会是:一次Review最好控制在三十分钟以内。超过三十分钟,人的注意力会急剧下降,再看下去要么漏掉问题,要么开始无意义地抠字眼。所以如果PR大到看不完,先不要硬看完,回到PR描述里要求开发者拆分,这比硬着头皮看完更负责任。
关于Review心态,我想给新手Reviewer一个建议:不要把自己当成裁判,要把自己当成“第一个使用者”和“未来的维护者”。你提出的每一条意见,本质上是在替三个月后会读到这段代码的同事发声。所以与其说“你这里写得不对”,不如试试“这个逻辑我理解起来有点费力,能不能换个表达方式,让下一个维护者更好上手”。同样一个意思,换种说法,抵触感会小很多。
关于被review的人,我也有一句话想说:不要把Review意见当成对你的指责。我见过太多开发者在评论区跟Reviewer来回拉扯十几个回合,最后就为了证明自己的写法没错。实际上的很多时候,Reviewer的“疑问”并不代表你的代码是错的,它只说明这段代码的表达没有达到“自解释”的程度。这恰恰是成长的机会。
最后再分享一个小技巧。我在open-code-review里给每个Reviewer加了一条隐性标准:假设这个项目明天就交给一个完全不了解历史背景的新人,他能否仅仅通过PR描述、代码注释和测试用例,就理解这次变更的来龙去脉?如果能,这个PR的质量就是合格的。这是我的个人经验,你可以拿去试试。
如果这篇文章对你有帮助,或者你也在做类似的代码审查流程建设,欢迎交流。工具和流程会过时,但“让每一次代码变更都被认真对待”这件事,在任何团队里都是值得投入的。