“代码审查”这四个字,在很多团队里已经快变成一个让人提不起劲的词。一说要做 code review,有人觉得是流程部门在给自己加戏,有人把它当成上线前的最后一道签字,只求快点看到 Approve。我在很长一段时间里也是这种心态,直到一次线上事故彻底改变了我的看法。那次以后,我在团队里认真搭建了一套开放式的代码审查机制,也就是 open-code-review:每次代码合入主分支之前,至少有一双以上的眼睛在盯着;任何角色都能参与评论,而不是只有“负责人”说了算;审查过程中产生的经验,会沉淀成清单和规则,持续作用于后续的每一次提交。这篇文章适合正在带团队的技术负责人、想改进质量流程的工程师,以及那些想逃离“审查等于走形式”困局的人。这里面的每一条,都是我实际踩过之后才总结出来的,不少内容在官方文档里看不到。
1. 一场线上事故教会我的事:审查不是流程点缀
1.1 一行业务代码引发的连锁故障
那次事故的背景并不复杂,一个订单状态流转的功能,改动量据说只有十几行。开发同学在本地测了正常路径,测试环境也过了,上线后却在某个边缘场景下出现了重复回调,订单状态被覆盖回更早的阶段,引发了一连串对账异常。排查到最后,问题出在一个并发分支上:两个线程同时读到旧状态,都认为自己是合法的状态迁移者,先到先写,后到也写,最终数据被回退了。
这个 bug 在代码评审环节本来有机会被发现。哪怕只是多一个人问一句“这段逻辑在并发场景下有没有问题”,结果都会不同。但当时的实际情况是,这个提交很小,看起来人畜无害,组里没人仔细看,直接就合了。事后复盘时,团队里有个同学一针见血:“我们不是没有流程,我们是把流程当作摆设。”
那次事故之后,我开始认真思考一个此前一直被忽略的问题:代码审查到底应该防住什么,又应该如何设计,才能让大家真的去用、愿意去用,而不是当作负担。
1.2 审查真正在防的四类问题
很多人对代码审查的理解停留在“找 bug”,这其实低估了它的价值,也高估了它的定位。靠人工审查来抓所有 bug 是不现实的,静态分析工具和单测在这个层面更廉价、更稳定。审查真正值得投入精力的,是下面四类问题,它们恰恰是自动化工具很难覆盖的:
第一类是逻辑与设计问题。数据结构选得对不对,接口边界划分得是否合理,状态机迁移是否完备,这些需要人基于上下文来判断,工具很难给出准确评估。
第二类是并发与事务问题。竞态条件、死锁、事务边界错误,这类问题在单机单线程的本地测试中几乎无法暴露,往往要等到高并发线上环境才现出原形。代码审查是少数几个能在早期发现它的手段之一。
第三类是可维护性问题。命名是否表意、函数是否过长、是否复制粘贴了大段重复逻辑、模块依赖是否合理。这些问题不直接影响上线,但会在未来半年内持续消耗团队所有人的时间去理解。
第四类是安全隐患。越权查询、未校验的外部输入、敏感信息硬编码、缺失权限校验。安全问题通常不是开发者故意埋雷,而是没有意识到“这个接口可能被任何人调用”。
这四类问题有一个共同点:它们都高度依赖上下文和意图。审查者必须理解这次改动的目的、边界和取舍,才能给出有质量的判断,这也是我后来提出“开放审查”的出发点——不同背景的人看同样的代码,会看到不同的盲区。
1.3 “开放”和“小圈子检查”的本质区别
传统的审查模式,往往是“负责人审开发者的代码”。这个模式最大的问题在于,负责人和开发者长期面对同一套业务逻辑,思维会逐渐趋同,很多被两个人默认接受的“老问题”,会在代码里沉淀很久不被发现。就像两个都习惯了房间里霉味的人,谁也察觉不到不对劲。
open-code-review 强调的“开放”,首先体现在审查者不限于固定的几个人。任何一个对这部分代码有疑问、有经验的人,都可以提评论。前端可以去看后端的接口契约,测试可以去看业务逻辑的覆盖度,新人也可以去读代码并提出自己的理解是否正确。其次体现在审查过程是双向对话,作者要回应每一条评论,要么采纳,要么说明理由。最后体现在审查经验是公开沉淀的——每一条有价值的评审意见,都可能转化为检查清单、代码规范或者自动化规则,而不是留在某个人的私人大脑里。
这种开放式审查的收益,在初期可能看不出什么,但坚持三个月以上,团队对代码的整体理解水平会有肉眼可见的提升。
2. 把审查流程从“靠自觉”做成“流水线”
2.1 最小可用流程:三道关卡就够
很多团队把代码审查复杂化了,又是评审会议,又是多人签字,又是各种审批角色。我的经验是,在一二十人的研发团队里,一套能长期执行的流程必须极轻,轻到开发者不觉得烦,管理者不需要催。我现在在团队里推的流程,总结起来就三道关卡。
第一道关卡是分支策略约束。所有的代码变更都必须通过合并请求(GitHub 叫 Pull Request,GitLab 叫 Merge Request,以下简称 PR/MR)合入受保护分支,不允许任何人直接 push 到主干。这一条是硬性的,靠平台权限配置来保证,而不是靠团队自觉。
第二道关卡是人工审查。每个 PR/MR 至少需要一名审查者明确 Approve 才能合入。这里的关键词是“至少一名”,如果改动涉及多个模块,合理的做法是每个受影响的模块都安排人看一眼。审查者需要是熟悉该模块的人,但绝对不是开发者本人。
第三道关卡是自动化状态检查。单元测试、静态检查、构建流程都要接入到合并请求上,任何一项失败都不能合入。这里注意,自动化的作用是兜底,保证“基础不破坏”,而不是替代人工审查。
这套流程看起来平淡无奇,但真正的难点在于执行细节。比如“审查者如何产生”这个看似简单的问题,如果完全靠开发者自己找人,很快就会出现两种情况:一种是永远找关系好的同事,另一种是找不到人干脆拖着。所以流程必须内置“分配机制”,而不是“等靠要”。
2.2 审查者分工:负责人、专家与学习型旁观
开放的代码审查不是让所有人都围观每一个改动,那既不现实也没有必要。我通常会把审查者分为三种角色。
第一种是模块负责人,也就是对这部分代码长期负责的人。他们的审查重心是“这次改动是否符合模块既有的架构方向,会不会埋下长期债务”。这个角色有最终拍板权。
第二种是领域专家。比如一次涉及支付金额计算的改动,除了模块负责人,还要拉上对支付和财务逻辑最熟的人。他们不一定熟悉模块的代码结构,但他们对业务规则的理解,能填补开发者的认知盲区。
第三种是学习型旁观者。这个角色通常由新人或者跨组协作的同事来扮演。他们没有压力,不需要 Approve,只看和问。我经常鼓励新人去读一些核心模块的 PR/MR,看老手的代码讨论,这比啃任何文档都更能让人快速理解系统的真实逻辑。
在实际配置里,模块负责人用平台机制来自动指定,领域专家靠作者主动拉取,学习型旁观者则完全自愿。三种角色的存在,才让“开放”变得有秩序,而不是一场混乱。
2.3 分支策略与合入门禁的取舍
谈到分支策略,我见过最省心的组合是:主干分支或者短周期特性分支,配合强制 PR/MR 审查和 CI 状态检查。Git Flow 那种长驻 develop、release、hotfix 多分支并存的模式,在大团队、强发布节奏下有其价值,但对于大多数中小团队,它带来的合并成本会直接侵蚀审查的意愿。
我的核心原则是分支状态永远短暂。一个 PR/MR 在合并队列里待的时间越久,合并冲突的概率越大,改动和主干的思想模型偏差也越大。所以合入门禁里我会设置一条:PR/MR 必须是最新的,也就是说它必须已经合并了主干的最新状态,否则要刷新后重新跑检查和审查。
这一点很多团队会忽略,导致明明审查时是好的代码,合入时却和别人的改动产生了冲突,最后带着冲突强合,审查事实上被架空了。解决方式很简单,在分支策略里开启“要求分支是最新的”规则,合入前的任何过期代码都看不见 Approve 按钮。
3. 具体落地工具选型:适合自己的才是好方案
3.1 GitHub、GitLab、Gerrit 与 Gitea 的取舍
聊代码审查离不开工具。我用 GitHub、GitLab、Gerrit 都搭过完整的审查流程,也说下各自的感受。
| 特性 | GitHub | GitLab | Gerrit | Gitea |
|---|---|---|---|---|
| 审查模式 | PR + 评论 + Review 多人审批 | MR + 评论 + 多级审批 | Push 后自动生成审查集,强制三方角色 | PR 基础功能,插件生态较薄 |
| 上手成本 | 低 | 中 | 高 | 低 |
| 自托管难度 | 需要企业版或走云端 | 社区版功能相对完整 | 自托管为主,配置复杂 | 极轻量 |
| 适合场景 | 中小团队、开源项目 | 中大型团队、需要 DevOps 一体化 | 对提交粒度控制极严格的团队 | 轻量自托管、极简社区 |
对于多数中小团队,我一般建议直接用 GitHub 或者 GitLab 的企业版/社区版,不要一开始就上 Gerrit。Gerrit 的审查模型是“每个 commit 独立审查,合入后生成新 commit”,这种设计在严谨的底层库维护中有优势,但对业务迭代来说太重了。用朋友的话说,在 Gerrit 里改一行代码可能要经历十轮评论刷新,不是所有团队都吃得住这套流程。
Gitea 更适合那种资源紧张、团队规模很小、只需要最基础 PR 能力的场景。它的插件生态无法和 GitHub 生态同日而语,但如果目标仅仅是“有一个人看代码再合入”,完全够用。
3.2 CODEOWNERS 与自动分配:让审查者不再靠缘分
谈到工具配置,第一个值得花时间做好的就是代码归属规则,对应 GitHub 的 CODEOWNERS 文件。这个文件写在仓库根目录,用来告诉平台“哪个路径的代码由谁负责审查”。一旦配置好,开发者创建 PR/MR 时,平台会自动把对应路径的负责组或个人拉进来当审查者。
举个例子,一个前后端分离的仓库,根目录的 CODEOWNERS 可以这么写:
# 默认所有未匹配路径由 backend 维护组负责 * @backend-team # 前端相关目录交给 frontend 组,覆盖上面的默认规则 src/ui/** @frontend-team # 数据库迁移文件必须由 DBA 审查 db/migrations/ @dba-team # 流水线或全局配置文件由平台组把关 .circleci/ .github/ @platform-team这个文件带来的好处是,审查者不再靠开发者在群里吼“帮我审一下”来确定,而是由路径归属自动决定。配合 GitHub 的 Branch protection 规则,把“至少一个审查者”和“CODEOWNERS 审查必须通过”同时勾上,审查这个动作就和代码路径强绑定,谁也跳不过去。
3.3 把 CI 状态检查当成合入的硬门槛
工具链的另一半是持续集成。很多人以为 CI 只是跑测试,其实它的角色应该是“合入门禁的一部分”。我推荐的组合是:静态检查、单元测试、构建验证、必要时加上容器镜像构建,全部挂在 PR/MR 状态检查上。
在 GitHub Actions 里,一个最小但完整的门禁配置大概是这样的意图:任何 PR 只要测试失败,合并按钮就不可用。在 GitLab 里类似,通过 Merge Request 的 Pipelines 配置,把 test 和 build 阶段设置为 required。关键点在于,状态检查的结果必须是合入的必要条件,而不是参考条件。我见过不少团队把 CI 跑出来的红灯当作“提醒”,依旧手动合并,这种形同虚设的集成,还不如不接。
另外我想强调一点,CI 的检查项不宜过多。每次提交都跑 30 分钟的全量回归,开发者的反馈循环会变得极长,这会诱导他们为了绕过等待而去做非合规操作。好的做法是把快速的单测和静态检查放在前置阶段,把耗时的集成测试抽出来放到夜间或发布前执行。这样既保证了门禁的有效性,又不至于让开发者觉得提交一次代码要等半天。
4. 一份能真正用的审查清单长什么样
4.1 清单是从每一次评论里长出来的
刚开始推代码审查时,我犯过一个典型错误:直接丢给团队一长串网上抄来的检查清单,覆盖范围从数据库索引到 CSS 命名,看起来无所不包,实际上没人愿意逐条对着看。后来我终于明白,真正能用的清单不是“设计”出来的,而是从真实的评审评论里长出来的。
我一般会每隔一两个月,把团队过去一段时间的评审评论全部过一遍,把那些重复出现过两次以上的问题挑出来。比如“这里 N+1 查询了”“这个接口漏了权限校验”“事务范围太长了”之类的评论反复出现,就说明团队在这个地方系统性薄弱,值得入清单。
这样做的好处特别直接:清单里的每一条,都对应着团队自己踩过的坑,战友们有真实记忆,更容易被认真对待。清单也是动态的,如果一个季度内某条规则再没有触发过,就可以考虑把它降级或移除,避免清单膨胀成没人读的废纸。
4.2 清单的三级分层与优先级
我设计的清单不是平铺的几十条,而是分成三级。这个分层非常重要,它决定了审查者在有限精力下应该先看什么。
第一级是阻断级。这类问题一旦存在,绝对不能合入。典型包括:数据丢失风险、安全漏洞、明显的内存泄漏或死锁风险、对线上数据不可逆的破坏性操作。阻断级问题出现的频率其实不高,但它就是整个流程的底线。
第二级是建议级。不直接影响上线,但会影响后续开发和维护的问题。比如缺少错误的处理分支、函数过长导致难以单测、重复代码逻辑、命名表意不明。建议级问题应当被提出并记录,但不一定非要当场改完,可以和作者约定后续技术债条目。
第三级是可选优化。纯风格偏好或锦上添花的建议。这一级的东西提的时候务必带语气词,明确告诉作者“不改也不影响合入”,避免作者因为鸡毛蒜皮的评论产生反感情绪。
4.3 可以直接复制的一套基础清单
下面这套清单是我目前在业务团队里实际使用的精简版,维度覆盖了逻辑、并发、安全、可维护性、性能、测试六块。你可以直接拿过去,再结合自己项目的评论历史做增删。
功能与逻辑
- 改动是否和需求描述一致,有没有顺带夹带无关修改
- 边界条件是否覆盖:空值、超长、重复提交、中间状态
- 错误处理是否完整:外部调用失败后会走什么分支
- 是否有删掉的代码仍然被其他地方引用
并发与事务
- 共享状态修改是否有锁或原子操作保护
- 事务边界是否清晰,事务内是否做了耗时操作
- 是否存在“先读后写”的竞态窗口
安全
- 外部输入是否经过校验或清洗
- 涉及用户数据的接口是否有越权检查
- 有没有把密钥、Token、连接串硬编码到代码里
- 日志和报错信息是否泄漏了敏感数据
可维护性
- 命名是否能让第一次读代码的人秒懂
- 函数是否太长,能否拆成有语义的多个步骤
- 有没有复制粘贴的重复代码
- 注释是否解释了“为什么”,而不是描述“做了什么”
性能
- 循环里是否发了 SQL 或外部请求(典型的 N+1 问题)
- 新查询是否命中索引
- 是否存在可以合并的请求或可以缓存的计算
测试
- 是否覆盖了核心分支和异常分支
- 测试断言是验证了真实行为,还是只为了跑通
- 测试自身是否稳定,会不会因依赖外部环境而闪断
建议把清单贴到团队的文档里,每次提交 PR/MR 时作者先自检一遍,审查者再对照着逐项过。这不是限制,更像是一张安全网。
5. 踩坑实录:审查频率、评论话术和遗留负担
5.1 评审对象体积失控:一个 MR 顶十个
实践中最常见也最致命的坑,是 PR/MR 的体量失控。我见过同事一整天的工作攒出一个 2000 行的改动,标题写着“完成 XX 功能”,实际塞了三个不同模块的重构、一个配置文件的批量修改和一段新业务入口。
大 PR/MR 对审查的摧毁是全方位的。从审查者视角看,2000 行代码要进入工作状态就得反复切换上下文,审到后面已经忘了前面讲了什么,最后只能草草扫一遍,把风险都留给线上。从作者的视角看,大改动意味着分支生命周期长,每天都要处理冲突,心态会越来越焦躁。
经验值建议:单个 PR/MR 控制在 200 到 400 行之间是最优区间。超过 800 行,审查质量就会急剧下降。如果你发现一个改动怎么拆都拆不小,那往往是设计阶段没有把任务拆细。这种情况下,应当回头把任务拆分重做,而不是硬着头皮交大 PR。
5.2 怎么让评论从“挑刺”变成“讨论”
早期我在团队里推动审查时,评论区经常变成辩论场,火药味很浓。后来我发现原因不在人,而在评论的表达方式。一个典型的“命令式评论”长这样:
“这里写反了,改成 xxx。”
另一个“提问式评论”长这样:
“这个分支在并发场景下会不会走到反向逻辑?我在本地模拟时总感觉会多跑一次回调,是不是我看漏了某个前置判断?”
两种表达方式看起来只是措辞差异,实际完全不一样。命令式评论把作者推到防守位,作者的第一反应是辩解;提问式评论把双方摆到共同解决问题的同一侧,作者会思考“是不是真的有问题”,哪怕是误报,讨论也会友好地结束。
另外要注意的是,审查评论应该给理由。不要只说“这个设计不行”,要说明“为什么不行,可能的后果是什么”。只给结论不给理由的意见,很难让人信服,也难以沉淀成团队共识。
5.3 审查积压的破局思路
团队推行审查一段时间后,最常见的运营问题是积压。PR/MR 开出来了,审查者半天没动静,发消息催又觉得不好意思。时间一长,作者会想方设法规避流程,甚至有人开始走“先合入、后补审批”的野路子。
解决积压的办法不是靠道德说教,而是靠机制。我们团队后来试了几种方案,其中最有效的是审查值班制:按天轮流指定一个值班审查者,当天的非阻塞性 PR/MR 都由值班者优先处理。同时把每个 PR/MR 的等待时间上限设为 24 小时,超过后自动提醒,再超过则升级到技术负责人。
有些公司在推行“不做完不回家”的所谓极限制度,我个人是很不推荐的。审查应该是有节奏的日常,而不是集中营式的突击。固定的值班轮换,比任何口号都更能维持审查的持续流动。
5.4 自动化工具拦在人工审查之前
最后想说的是自动化和人工审查的关系。静态检查工具的粒度越做越细,从格式规范到一些简单的空指针预判都能拦截,这本身是好事。但我也发现一个反噬现象:当自动化检查项过多、误报率偏高时,开发者会对所有检查结果都变得麻木,看到三条警告,连点开看的欲望都没有了,顺手就 Mark 成 ignore。
所以自动化检查的设计原则应该是少而准,可以为了准确率牺牲一部分覆盖面。高准确率的检查项才能建立信任,有了信任,开发者在提交代码时才会主动先跑一遍,而不是依赖合入时被 CI 拦下来再抱怨。
至于那些静态工具判断不了的问题——架构合理性、并发正确性、业务意图匹配度——才应该是人工审查集中精力盯的部分。把自动化做扎实,本质上是在为人省出精力,去处理机器搞不定的模糊判断。
6. 开放审查的进阶价值:知识流动与质量文化
6.1 新人和跨角色成员也能参与并产出价值
有段时间,团队里新来的同学几乎不参与任何代码讨论,他们觉得“我啥都不懂,谈什么”。但其实,新人的问题往往是最有价值的。他们不知道“历史包袱”的存在,所以会对一些老手习以为常的奇怪逻辑直接发问,而对老手来说,回答这些问题的过程,本身就是一次对既有假设的复审。
我在团队里做的事很简单:明确告诉新人,任何 PR/MR 你都可以去看,看不懂的代码可以直接在评论里问,不需要有压力,你提出来的问题不一定要和代码优化有关。开放审查对新人来说,就是一块免费且没有风险的学习黑板。他们读代码、看老手的讨论、提问、被解答,两三个迭代之后,对系统的熟悉程度能超过自己埋头看文档一个月。
测试同学参与审查也是个意外收获。他们通常不像开发那样对内部实现过度自信,而是带着“这台机器输入了 xxx,会不会得到 yyy”的测试视角去提问,往往能发现开发者自己完全没考虑到的异常路径。
6.2 从代码审查走向设计评审
开放审查成熟到一定程度,自然会向上游延展。当团队习惯了“写代码之前先被人质疑一下”,他们就会慢慢接受“写代码之前先把设计讲给人听”这件事。我们的实践是,对中等以上复杂度的需求,先开一个 15 分钟到 30 分钟的轻量设计评审,不需要完整的方案文档,只需要把事情的关键决策点、数据流、接口边界讲清楚,让大家在进入代码之前输入意见。
这一步带来的价值怎么夸都不过分。许多问题在设计阶段被拦下,比在代码审查阶段再发现要节省数倍成本。而且设计评审的经验导入了代码审查,开发者开始主动在设计上对齐,而不是边写边临时调整。在整个质量链条里,设计评审处在最上游,而代码审查是中游的承重墙,两者配合起来,才能让质量不是靠漏洞发现来兜底,而是靠源头规避来保证。
6.3 指标观测要克制:让数据服务判断
谈到质量和流程,几乎避不开指标这件事。常见的指标有审查覆盖率、平均审查时长、每百行代码评论数、评审发现缺陷数等。这些指标有用,但必须节制地使用,否则很容易被数据绑架。
我见过一个团队把“审查覆盖率”当成了核心 KPI,结果大家的操作变成了每个 PR/MR 都草草点 Approve,覆盖率漂亮得吓人,但质量问题一个没少。指标本身不会带来质量,它只是帮助你理解流程运转的信号。我更建议关注的是:审查者提出并得到响应的评论数和平均处理时长,因为这两个指标直接反映了审查的互动质量和流动效率,而不是被“合入率 100%”这种假象迷惑。
一个健康的审查文化是让数据成为持续改进的仪表盘,而不是考核的大棒。数据暴露问题后,回到清单和流程里找原因,去调制度,这才是闭环。
最后再讲一点我的真实体会
把开放式代码审查真正落地,我最满意的一个变化是:团队在讨论代码时,不再觉得有人在“挑刺”,而是默认每个人都是想把东西做好的同行者。一次审查留下的未必只有代码修订,还有一个人对另一个人的理解。这个变化没法靠流程强制实现,只能靠机制设计释放善意,再加时间慢慢发酵。
如果你正在团队的审查困境里挣扎,我的建议是从小处开始:先选定一个仓库,配好分支保护和 CODEOWNERS,把默认的审查人数设为 1,再定一个两周回顾的节奏,从真实的评论里迭代你的审查清单。不用追求一步到位,也不需要一开始就铺到所有仓库、所有团队,先从一边跑通,慢慢扩开。这样既不会引发大规模反弹,也能在最短时间内攒出可以直接说明问题的数据样本。