前阵子我接手一个历史遗留模块,代码里躺着一行注释:“这段逻辑不要动,动了会出线上事故。”没有署名,没有日期,也没有任何原因说明。我盯着它看了很久,最后还是把那段重构掉了——结果注释里担心的问题,早在三个版本之前就被另一个修复解决了。那行注释唬住了之前至少两个想改代码的人,也让我在排查时白白兜了一圈。今天这篇想聊聊注释与代码格式化。注释管的是语义层的可读性,格式化管的是结构层的可读性,它们看起来是最基础的两个话题,却是我在代码评审里看到争议最多、误解最深的地方。这篇文章适合刚接触团队协作的新人,也适合正在为“要不要删注释”“要不要统一格式化工具”吵得不可开交的团队。我会把注释的适用场景、反模式、验收标准,以及格式化工具的落地链路和常见坑都拆开讲一遍。内容偏实操,以我这些年看代码、写代码、改代码的真实感受为主。1. 注释究竟是什么:它补的是表达力,不是写作量1.1 先分清代码的第一表达通道我一直坚持一个观点:代码本身才是第一表达通道,注释是次要的兜底手段。变量名、函数名、模块边界、参数顺序、返回类型,这些才是每天要被读几十遍的东西。一个函数如果叫validateOrderStatus,它做的事情已经说了大半,这时候你再在旁边写“// 验证订单状态”,就是纯粹的复读机。真正需要注释的地方,往往是代码无法独立表达的“为什么”。比如一个校验逻辑必须先查数据库再查缓存,因为缓存每天凌晨才刷新一次。这段因果链埋在三层函数调用里,单看某一行的代码根本猜不出来。这时候注释就是在补表达力的缺口,不是在凑字数。我常用的判断标准很简单:如果删掉这行注释,读者仍然能通过代码结构、命名、测试用例推断出同样的信息,那它就没有存在价值。如果删掉后读者会产生误解,或者根本无法判断前置条件,那它才有资格留下来。1.2 写注释的真正时机在我见过的代码里,真正有价值、删不得的注释通常只出现在三类位置。第一类是公共 API 的边界。别人调你的函数,不需要读完整实现,但需要知道传入参数的取值范围、返回值的含义、会抛出什么异常。这种注释写在接口签名旁边,相当于给调用者一份“使用手册”。第二类是业务规则背后的隐藏约束。典型例子是“这里不能走缓存,因为实时性要求高”或“必须先更新订单状态再扣库存,否则对账会不平”。这些约束往往来自一场漫长的需求讨论,代码本身看不到理由。第三类是排查问题后沉淀下来的因果结论。比如“此处 Service 的实例是以接口方式注入,千万不要改成实现类注入,否则启动阶段的循环依赖会复现”。这类注释是我最珍惜的,因为它是用线上故障换来的经验。1.3 给注释设定的验收标准我自己写注释之前会做三个检查。第一问,这行注释丢了一年后,后面的人会不会因为少了它而做错事?如果答案是不会,那就不写。第二问,这个注释是回答 why 还是回答 what?what 应该由代码自己回答,why 才需要人补充。第三问,这行注释会不会在业务变更后过期?如果可能过期,我要么把“失效条件”写进去,要么干脆不写。有一种注释很容易被忽略:它写的时候是对的,但代码后来变了,注释没人同步更新,它就变成了一颗定时炸弹。所以注释不只是写的时候要克制,维护的时候更要用对待代码一样的态度去对待它。看到注释与代码不符时,不是顺手改一下代码,而是要追问哪边才是当前的真相。2. 最该删掉的四种注释:评审现场几乎天天见2.1 复读机式注释:删掉无损失我在 Code Review 里见过最多的,就是下面这类注释:let count = 0; // 计数器 for (const user of users) { // 遍历所有用户 process(user); }这种注释单独看无害,但放在一起就会形成严重的视觉噪音。人眼扫过代码时,大脑会不自觉地把注释也读进去,信息量明明只有一行,视野里却堆了三行。密集的复读注释会稀释真正重要的代码和真正重要的注释。它们就像网页里的无效广告,不提供任何信息,只是挡住视线。遇到这种注释,我通常直接一整片删掉,没有任何心理负担。2.2 圈地式注释与年表式注释还有一类注释长这样:// ========== 初始化模块 ========== // ---------- 分界线 ---------- // 2023-01 某A 新增登录逻辑 // 2023-05 某B 修复支付回调问题圈地式注释在编辑器没有代码折叠功能的年代还有存在意义,它能帮你快速定位代码块的边界。但现在的 IDE 早就有结构折叠、书签、全局搜索,再靠一长串横线来导航,完全是重复劳动。更麻烦的是,每次移动函数、重构模块,你都得记得同步挪这些线条,忘一次就成了错位导航。年表式注释更没必要。版本控制系统的提交记录已经把每一次变更的日期、作者、原因、差异全记下来了,注释里的“谁在什么时候改了什么”是版本库的劣质复述。留着它,还会让评审时有人惯性去读这段年表,忽略真正的变更内容。同样的信息,git log和git blame查得又快又全,而且永远不会因为代码挪了位置而丢。2.3 误导性注释:比没有注释更危险没有注释,读者至少会谨慎地读代码。有了注释,读者会天然地信任它,哪怕代码和注释已经对不上。有一次我排查一个数据一致性问题,看到接口上方赫然写着“本接口只用于旧版客户端兼容,新端请调用新接口”。我放心地跳过了这个方法,结果查了整整一个下午才发现,那条“只用于旧客户端”的路径其实早就被新逻辑复用了,注释是半年前改版时留下的,代码在注释之后又改过三轮。那个下午我唯一的收获,就是对一个老观点产生了刻骨铭心的认同:代码至少会被编译器检查,注释永远没人检查,所以注释的可信度天然低于代码。遇到误导性注释,正确的动作是先确认代码当前的真实意图,再用 git 历史确认这条约束是已经失效还是仍然成立,最后决定是删掉注释还是改写注释。最忌讳的就是注释和代码打架时顺手把代码改回注释描述的样子,那样可能把已经修好的逻辑又改坏了。2.4 被注释掉的代码:版本库里的僵尸“这段先注释掉,以后可能用回来。”这句话我听过太多次,但真实情况是:注释掉的代码极少被用回来,它们只是在文件里慢慢腐烂。被注释的代码有几个隐蔽危害。第一,它会让搜索变得不可靠,你搜一个函数名,结果这个函数已经注释了两百行,你很难判断它到底是废弃的还是临时关闭的。第二,它在评审里制造噪音,别人必须花时间判断“这段是死的还是活的”。第三,它给人错误的心理暗示:代码是临时的,所以不需要认真对待。版本库本身就是最好的恢复机制。你删除的每一段代码,在历史提交记录里都完整保留着,真要找回,git log加git revert随时可以做到。所以“怕删了找不回来”这个顾虑在 git 时代根本不成立。我的做法是:看到注释掉的代码,先问作者要不要,不要就当场删,绝不手软。3. 格式化的自动化:把人类从无意义的争论中解放出来3.1 手工对齐是在浪费注意力写代码久了你会意识到,手工格式化是一个巨大的时间黑洞。团队里有人喜欢等号两边加空格,有人不喜欢;有人坚持函数参数超过三个就换行,有人觉得五个以内都应该放一行;有人用两个空格缩进,有人用四个。这些争论发生在代码评审里,一句“你觉得这边是不是该换个行”就能扯出十分钟讨论,而讨论的对象跟业务逻辑没有任何关系。格式化工具的使命不是让你喜欢它的输出结果,而是让团队根本不需要讨论结果。它把“唯一且自动”变成默认选项,把“缩进几个空格”“单引号还是双引号”“分号加不加”这类问题彻底移出评审清单。最初你会觉得工具的输出不好看,但很快你会发现,审美分歧让位于一致性之后,人的注意力终于可以集中在真正的设计问题上。3.2 格式化工具的根本逻辑:基于语法树,不是文本替换很多人以为格式化工具只是把文本按规则重新排一下,其实不是。主流工具都是先解析源码,生成抽象语法树,再按规则重写输出。这意味着它可以正确识别字符串、注释、模板字面量里的内容,不会因为里面有“{”或“;”就误判结构。正则替换做不到这一点,这也是那些粗暴的“代码美化脚本”经常把代码改坏的原因。常见语言和工具对应起来大概是这样:语言 / 生态格式化工具特点JavaScript / TypeScriptPrettier、ESLint --fix覆盖面广,配置项多,社区标准成熟PythonBlack配置极少,号称“不可配置”Gogofmt官方强制格式,Go 社区默认遵守Rustrustfmt官方工具,与语言版本绑定C / C++clang-format可定制性强,适合接入遗留项目Javagoogle-java-format 等风格统一,方便团队对齐选工具时不要只看“哪个生成的代码好看”,要看团队能不能统一接受它的输出风格。Go 社区的成功已经证明了一点:当格式化成为语言规范和工具链的默认部分,关于代码长相的争吵就会消失,新人和老人写出来的代码看起来几乎一样,code review 的效率直接上了一个台阶。3.3 格式化的极限:它能做的和不能做的格式化能解决的是结构规则:缩进、换行、空格、引号、分号、尾逗号。它不能解决的是真正的可读性问题:一个 300 行的函数要不要拆成三个;一个变量该叫flag还是shouldNotifyUser;一段状态机逻辑是否应该改用策略模式。这些问题的答案依赖业务判断和设计经验,机器无法自动决定。理解这个边界很重要。很多人把格式化工具当成代码质量的银弹,结果发现它跑完之后代码只是“整齐的难读”,于是转身说格式化没意义。这个结论下得太早了。格式化的价值在于把结构层的琐碎问题清零,让你在 review 时只需要关注语义层——注释、命名、函数划分、业务逻辑。它清理掉了噪声,但并没有替你产生信号。4. 团队落地格式化:从零到一的全过程4.1 分三步推进,别一步到位给团队引入格式化工具,最忌讳的就是第一天就把它挂进 CI,然后所有人 commit 失败。正确做法是分三步走。第一步,先把格式化命令写进项目的脚本里,比如在package.json里加一行:"scripts": { "format": "prettier --write \"src/**/*.{ts,tsx}\"" }这一步让团队里愿意用的人可以先跑起来,同时形成“格式化是项目的一部分”这个共识。第二步,接进提交钩子,让本地 commit 前自动格式化暂存区文件。这一步要特别注意只处理暂存区,不要一跑就把全仓库都格式化了。常见组合是 lint-staged 加 Husky,配置大概是:npx lint-staged --allow-empty配置里写成对*.js、*.ts、*.css等文件执行格式化命令。这样一次提交里只会包含你真正改动过的文件的格式化结果,不会产生大面积无关 diff。第三步,等团队已经适应了本地格式化,再在 CI 里加检查。CI 的任务是兜底——防止有人通过git commit --no-verify绕过本地钩子,把没格式化的代码推到远程。到这一步,格式化才真正成为团队纪律。4.2 提交钩子里最容易踩的三个坑第一个坑是全局格式化。团队里如果有人直接对全项目跑了一次格式化,提交信息里会混入几百个无关文件的改动,评审人员根本没法判断哪些是业务变更、哪些只是格式变化。解法就是我上面说的 lint-staged——只处理本次提交涉及的文件,把格式化范围收缩到最小。第二个坑是工具版本不一致。不同版本的 Prettier、Black 在某些边界情况下的输出可能不同,本地格式化没问题,CI 里一跑就报错。这个问题很隐蔽,因为大多数格式化工具升级时不会大张旗鼓地通知你。解法是锁版本,把格式化工具的精确版本放进项目依赖和 CI 环境里,升级工具版本时单独提交一次,顺便让全组重新格式化。第三个坑是跳过钩子。git commit --no-verify是本地命令,任何人随时都能绕过。这不是道德问题,而是流程设计问题——你必须假设会有人这么做。CI 检查就是用来兜住这个风险的,它管不到每个人的本地环境,但只要代码进了远程分支,就必须过格式校验。4.3 老项目怎么渐进式格式化老项目格式化是最需要耐心的事。如果一次性全量格式化,git 历史里的 blame 信息会被全面冲掉,以后想查某一行代码是什么时候、因为什么原因改的,会非常困难。我经历过一个项目,格式化提交合进去之后,所有人查 blame 都看到的是那一次格式化提交,真正的业务变更完全被淹没了。推荐的做法是分目录分阶段推进。先把一个子目录格式化成新风格,单独提交一次;再格式化下一个目录,再独立提交一次。每个格式化提交独立存在,不要和业务提交混在一起。同时,在仓库里维护一个.git-blame-ignore-revs文件,把每一次纯格式化的提交 hash 记进去。之后执行 git blame 时带上忽略文件,git 就会自动跳过那些格式化提交,继续追溯格式化之前的原始变更。这里是个典型配置流程:git config blame.ignoreRevsFile .git-blame-ignore-revs这个细节知道的人不多,但对历史包袱重的代码库非常关键。格式化本身是为了让代码更易维护,如果它反而毁掉了历史追溯能力,代价就大了。4.4 格式化讨论边界:把分歧留给真正的代码设计即使有了格式化工具,团队里关于“可读性”的讨论也不会完全消失,这其实是好事。区别在于,没有工具的时候,讨论的是一千个人有一千种偏好的流水账;有工具之后,讨论聚焦在真正值得讨论的设计问题上。举个例子,格式化工具可以决定三元表达式怎么写,但它不知道某个嵌套了三层的三元表达式是否应该拆成独立函数。工具能做的是强制执行“每条语句换行”,它做不到判断“这个逻辑的复杂程度已经超过了人类大脑的短期记忆容量”。所以我在评审时,一旦有人对某段代码的“读起来费劲”提出异议,我会顺着这个信号往深处挖:是不是函数太长了?是不是状态太多?是不是变量名没起好?这些才是人和人之间真正有意义的讨论。5. 我自己这几年关于注释与格式化的习惯5.1 看到可疑注释,我的处理顺序我现在看到一条可疑注释,不会立刻改代码,也不会立刻删注释。我会先用 git blame 找到这条注释是哪个提交引入的,再看关联的提交说明和 diff,搞清楚注释在写下的那一刻对应的是什么约束。然后我再对照当前代码,判断这条约束是否仍然成立。最后才决定是删除、改写,还是原样保留。这个流程能避免两个常见错误:一是误删还有价值的业务规则,二是保留已经失效的误导信息。注释不是不可动的文物,它需要和代码一样接受审查。5.2 新写代码时的注释原则我给自己立了一个很简单的规矩:能用命名说清楚的,绝对不用注释;能用结构说清楚的,也绝对不用注释;只有当“为什么这样做”只能靠语言表达的时候,才动用注释。而且这样的注释,我会尽量把有效期限写出来。比如“该兼容逻辑只在旧版本客户端存在期间有效,旧版本下线后可以删除”,这句话比单纯写“这里做兼容”有价值得多,因为它给了后人一个明确的清理信号。5.3 评审注重的四个检查项最后分享一下我在评审代码时,关于注释和格式化的固定检查清单。第一,注释是不是在复述代码做了什么。是的话,要求删掉。第二,注释里有没有“为什么”。没有的话,看看是否需要补一条。第三,注释和代码是否一致。不一致的话,要求当场解决,而不是留到以后。第四,格式化工具是否已经运行。没有运行的话,让提交者先跑完再提交。这四条检查项执行久了,团队会慢慢形成默契:注释是稀缺资源,只留给重要信息;格式化是自动流程,不需要人操心。等到这两种状态都成为日常,我发现自己写代码的心态也变了——我不再需要费心维护那些视觉上的整齐,也不用时刻担心一行注释会不会误导三个月后的自己。注意力被彻底解放出来,放在真正需要人做判断的事情上:这个模块边界是否清晰,这段逻辑是否真的做了它该做的事。格式化工具用起来可能只需要半天,但把“注释要克制、格式要自动”这两条刻进习惯,值得花很长时间反复练。每次代码评审都是一次训练,每次重构都是一次验证。慢慢地你会发现,干净代码不是靠某一招实现的,而是这些看起来不起眼的小事,一件一件累积出来的。
前阵子我接手一个历史遗留模块,代码里躺着一行注释:“这段逻辑不要动,动了会出线上事故。”没有署名,没有日期,也没有任何原因说明。我盯着它看了很久,最后还是把那段重构掉了——结果注释里担心的问题,早在三个版本之前就被另一个修复解决了。那行注释唬住了之前至少两个想改代码的人,也让我在排查时白白兜了一圈。今天这篇想聊聊注释与代码格式化。注释管的是语义层的可读性,格式化管的是结构层的可读性,它们看起来是最基础的两个话题,却是我在代码评审里看到争议最多、误解最深的地方。这篇文章适合刚接触团队协作的新人,也适合正在为“要不要删注释”“要不要统一格式化工具”吵得不可开交的团队。我会把注释的适用场景、反模式、验收标准,以及格式化工具的落地链路和常见坑都拆开讲一遍。内容偏实操,以我这些年看代码、写代码、改代码的真实感受为主。1. 注释究竟是什么:它补的是表达力,不是写作量1.1 先分清代码的第一表达通道我一直坚持一个观点:代码本身才是第一表达通道,注释是次要的兜底手段。变量名、函数名、模块边界、参数顺序、返回类型,这些才是每天要被读几十遍的东西。一个函数如果叫validateOrderStatus,它做的事情已经说了大半,这时候你再在旁边写“// 验证订单状态”,就是纯粹的复读机。真正需要注释的地方,往往是代码无法独立表达的“为什么”。比如一个校验逻辑必须先查数据库再查缓存,因为缓存每天凌晨才刷新一次。这段因果链埋在三层函数调用里,单看某一行的代码根本猜不出来。这时候注释就是在补表达力的缺口,不是在凑字数。我常用的判断标准很简单:如果删掉这行注释,读者仍然能通过代码结构、命名、测试用例推断出同样的信息,那它就没有存在价值。如果删掉后读者会产生误解,或者根本无法判断前置条件,那它才有资格留下来。1.2 写注释的真正时机在我见过的代码里,真正有价值、删不得的注释通常只出现在三类位置。第一类是公共 API 的边界。别人调你的函数,不需要读完整实现,但需要知道传入参数的取值范围、返回值的含义、会抛出什么异常。这种注释写在接口签名旁边,相当于给调用者一份“使用手册”。第二类是业务规则背后的隐藏约束。典型例子是“这里不能走缓存,因为实时性要求高”或“必须先更新订单状态再扣库存,否则对账会不平”。这些约束往往来自一场漫长的需求讨论,代码本身看不到理由。第三类是排查问题后沉淀下来的因果结论。比如“此处 Service 的实例是以接口方式注入,千万不要改成实现类注入,否则启动阶段的循环依赖会复现”。这类注释是我最珍惜的,因为它是用线上故障换来的经验。1.3 给注释设定的验收标准我自己写注释之前会做三个检查。第一问,这行注释丢了一年后,后面的人会不会因为少了它而做错事?如果答案是不会,那就不写。第二问,这个注释是回答 why 还是回答 what?what 应该由代码自己回答,why 才需要人补充。第三问,这行注释会不会在业务变更后过期?如果可能过期,我要么把“失效条件”写进去,要么干脆不写。有一种注释很容易被忽略:它写的时候是对的,但代码后来变了,注释没人同步更新,它就变成了一颗定时炸弹。所以注释不只是写的时候要克制,维护的时候更要用对待代码一样的态度去对待它。看到注释与代码不符时,不是顺手改一下代码,而是要追问哪边才是当前的真相。2. 最该删掉的四种注释:评审现场几乎天天见2.1 复读机式注释:删掉无损失我在 Code Review 里见过最多的,就是下面这类注释:let count = 0; // 计数器 for (const user of users) { // 遍历所有用户 process(user); }这种注释单独看无害,但放在一起就会形成严重的视觉噪音。人眼扫过代码时,大脑会不自觉地把注释也读进去,信息量明明只有一行,视野里却堆了三行。密集的复读注释会稀释真正重要的代码和真正重要的注释。它们就像网页里的无效广告,不提供任何信息,只是挡住视线。遇到这种注释,我通常直接一整片删掉,没有任何心理负担。2.2 圈地式注释与年表式注释还有一类注释长这样:// ========== 初始化模块 ========== // ---------- 分界线 ---------- // 2023-01 某A 新增登录逻辑 // 2023-05 某B 修复支付回调问题圈地式注释在编辑器没有代码折叠功能的年代还有存在意义,它能帮你快速定位代码块的边界。但现在的 IDE 早就有结构折叠、书签、全局搜索,再靠一长串横线来导航,完全是重复劳动。更麻烦的是,每次移动函数、重构模块,你都得记得同步挪这些线条,忘一次就成了错位导航。年表式注释更没必要。版本控制系统的提交记录已经把每一次变更的日期、作者、原因、差异全记下来了,注释里的“谁在什么时候改了什么”是版本库的劣质复述。留着它,还会让评审时有人惯性去读这段年表,忽略真正的变更内容。同样的信息,git log和git blame查得又快又全,而且永远不会因为代码挪了位置而丢。2.3 误导性注释:比没有注释更危险没有注释,读者至少会谨慎地读代码。有了注释,读者会天然地信任它,哪怕代码和注释已经对不上。有一次我排查一个数据一致性问题,看到接口上方赫然写着“本接口只用于旧版客户端兼容,新端请调用新接口”。我放心地跳过了这个方法,结果查了整整一个下午才发现,那条“只用于旧客户端”的路径其实早就被新逻辑复用了,注释是半年前改版时留下的,代码在注释之后又改过三轮。那个下午我唯一的收获,就是对一个老观点产生了刻骨铭心的认同:代码至少会被编译器检查,注释永远没人检查,所以注释的可信度天然低于代码。遇到误导性注释,正确的动作是先确认代码当前的真实意图,再用 git 历史确认这条约束是已经失效还是仍然成立,最后决定是删掉注释还是改写注释。最忌讳的就是注释和代码打架时顺手把代码改回注释描述的样子,那样可能把已经修好的逻辑又改坏了。2.4 被注释掉的代码:版本库里的僵尸“这段先注释掉,以后可能用回来。”这句话我听过太多次,但真实情况是:注释掉的代码极少被用回来,它们只是在文件里慢慢腐烂。被注释的代码有几个隐蔽危害。第一,它会让搜索变得不可靠,你搜一个函数名,结果这个函数已经注释了两百行,你很难判断它到底是废弃的还是临时关闭的。第二,它在评审里制造噪音,别人必须花时间判断“这段是死的还是活的”。第三,它给人错误的心理暗示:代码是临时的,所以不需要认真对待。版本库本身就是最好的恢复机制。你删除的每一段代码,在历史提交记录里都完整保留着,真要找回,git log加git revert随时可以做到。所以“怕删了找不回来”这个顾虑在 git 时代根本不成立。我的做法是:看到注释掉的代码,先问作者要不要,不要就当场删,绝不手软。3. 格式化的自动化:把人类从无意义的争论中解放出来3.1 手工对齐是在浪费注意力写代码久了你会意识到,手工格式化是一个巨大的时间黑洞。团队里有人喜欢等号两边加空格,有人不喜欢;有人坚持函数参数超过三个就换行,有人觉得五个以内都应该放一行;有人用两个空格缩进,有人用四个。这些争论发生在代码评审里,一句“你觉得这边是不是该换个行”就能扯出十分钟讨论,而讨论的对象跟业务逻辑没有任何关系。格式化工具的使命不是让你喜欢它的输出结果,而是让团队根本不需要讨论结果。它把“唯一且自动”变成默认选项,把“缩进几个空格”“单引号还是双引号”“分号加不加”这类问题彻底移出评审清单。最初你会觉得工具的输出不好看,但很快你会发现,审美分歧让位于一致性之后,人的注意力终于可以集中在真正的设计问题上。3.2 格式化工具的根本逻辑:基于语法树,不是文本替换很多人以为格式化工具只是把文本按规则重新排一下,其实不是。主流工具都是先解析源码,生成抽象语法树,再按规则重写输出。这意味着它可以正确识别字符串、注释、模板字面量里的内容,不会因为里面有“{”或“;”就误判结构。正则替换做不到这一点,这也是那些粗暴的“代码美化脚本”经常把代码改坏的原因。常见语言和工具对应起来大概是这样:语言 / 生态格式化工具特点JavaScript / TypeScriptPrettier、ESLint --fix覆盖面广,配置项多,社区标准成熟PythonBlack配置极少,号称“不可配置”Gogofmt官方强制格式,Go 社区默认遵守Rustrustfmt官方工具,与语言版本绑定C / C++clang-format可定制性强,适合接入遗留项目Javagoogle-java-format 等风格统一,方便团队对齐选工具时不要只看“哪个生成的代码好看”,要看团队能不能统一接受它的输出风格。Go 社区的成功已经证明了一点:当格式化成为语言规范和工具链的默认部分,关于代码长相的争吵就会消失,新人和老人写出来的代码看起来几乎一样,code review 的效率直接上了一个台阶。3.3 格式化的极限:它能做的和不能做的格式化能解决的是结构规则:缩进、换行、空格、引号、分号、尾逗号。它不能解决的是真正的可读性问题:一个 300 行的函数要不要拆成三个;一个变量该叫flag还是shouldNotifyUser;一段状态机逻辑是否应该改用策略模式。这些问题的答案依赖业务判断和设计经验,机器无法自动决定。理解这个边界很重要。很多人把格式化工具当成代码质量的银弹,结果发现它跑完之后代码只是“整齐的难读”,于是转身说格式化没意义。这个结论下得太早了。格式化的价值在于把结构层的琐碎问题清零,让你在 review 时只需要关注语义层——注释、命名、函数划分、业务逻辑。它清理掉了噪声,但并没有替你产生信号。4. 团队落地格式化:从零到一的全过程4.1 分三步推进,别一步到位给团队引入格式化工具,最忌讳的就是第一天就把它挂进 CI,然后所有人 commit 失败。正确做法是分三步走。第一步,先把格式化命令写进项目的脚本里,比如在package.json里加一行:"scripts": { "format": "prettier --write \"src/**/*.{ts,tsx}\"" }这一步让团队里愿意用的人可以先跑起来,同时形成“格式化是项目的一部分”这个共识。第二步,接进提交钩子,让本地 commit 前自动格式化暂存区文件。这一步要特别注意只处理暂存区,不要一跑就把全仓库都格式化了。常见组合是 lint-staged 加 Husky,配置大概是:npx lint-staged --allow-empty配置里写成对*.js、*.ts、*.css等文件执行格式化命令。这样一次提交里只会包含你真正改动过的文件的格式化结果,不会产生大面积无关 diff。第三步,等团队已经适应了本地格式化,再在 CI 里加检查。CI 的任务是兜底——防止有人通过git commit --no-verify绕过本地钩子,把没格式化的代码推到远程。到这一步,格式化才真正成为团队纪律。4.2 提交钩子里最容易踩的三个坑第一个坑是全局格式化。团队里如果有人直接对全项目跑了一次格式化,提交信息里会混入几百个无关文件的改动,评审人员根本没法判断哪些是业务变更、哪些只是格式变化。解法就是我上面说的 lint-staged——只处理本次提交涉及的文件,把格式化范围收缩到最小。第二个坑是工具版本不一致。不同版本的 Prettier、Black 在某些边界情况下的输出可能不同,本地格式化没问题,CI 里一跑就报错。这个问题很隐蔽,因为大多数格式化工具升级时不会大张旗鼓地通知你。解法是锁版本,把格式化工具的精确版本放进项目依赖和 CI 环境里,升级工具版本时单独提交一次,顺便让全组重新格式化。第三个坑是跳过钩子。git commit --no-verify是本地命令,任何人随时都能绕过。这不是道德问题,而是流程设计问题——你必须假设会有人这么做。CI 检查就是用来兜住这个风险的,它管不到每个人的本地环境,但只要代码进了远程分支,就必须过格式校验。4.3 老项目怎么渐进式格式化老项目格式化是最需要耐心的事。如果一次性全量格式化,git 历史里的 blame 信息会被全面冲掉,以后想查某一行代码是什么时候、因为什么原因改的,会非常困难。我经历过一个项目,格式化提交合进去之后,所有人查 blame 都看到的是那一次格式化提交,真正的业务变更完全被淹没了。推荐的做法是分目录分阶段推进。先把一个子目录格式化成新风格,单独提交一次;再格式化下一个目录,再独立提交一次。每个格式化提交独立存在,不要和业务提交混在一起。同时,在仓库里维护一个.git-blame-ignore-revs文件,把每一次纯格式化的提交 hash 记进去。之后执行 git blame 时带上忽略文件,git 就会自动跳过那些格式化提交,继续追溯格式化之前的原始变更。这里是个典型配置流程:git config blame.ignoreRevsFile .git-blame-ignore-revs这个细节知道的人不多,但对历史包袱重的代码库非常关键。格式化本身是为了让代码更易维护,如果它反而毁掉了历史追溯能力,代价就大了。4.4 格式化讨论边界:把分歧留给真正的代码设计即使有了格式化工具,团队里关于“可读性”的讨论也不会完全消失,这其实是好事。区别在于,没有工具的时候,讨论的是一千个人有一千种偏好的流水账;有工具之后,讨论聚焦在真正值得讨论的设计问题上。举个例子,格式化工具可以决定三元表达式怎么写,但它不知道某个嵌套了三层的三元表达式是否应该拆成独立函数。工具能做的是强制执行“每条语句换行”,它做不到判断“这个逻辑的复杂程度已经超过了人类大脑的短期记忆容量”。所以我在评审时,一旦有人对某段代码的“读起来费劲”提出异议,我会顺着这个信号往深处挖:是不是函数太长了?是不是状态太多?是不是变量名没起好?这些才是人和人之间真正有意义的讨论。5. 我自己这几年关于注释与格式化的习惯5.1 看到可疑注释,我的处理顺序我现在看到一条可疑注释,不会立刻改代码,也不会立刻删注释。我会先用 git blame 找到这条注释是哪个提交引入的,再看关联的提交说明和 diff,搞清楚注释在写下的那一刻对应的是什么约束。然后我再对照当前代码,判断这条约束是否仍然成立。最后才决定是删除、改写,还是原样保留。这个流程能避免两个常见错误:一是误删还有价值的业务规则,二是保留已经失效的误导信息。注释不是不可动的文物,它需要和代码一样接受审查。5.2 新写代码时的注释原则我给自己立了一个很简单的规矩:能用命名说清楚的,绝对不用注释;能用结构说清楚的,也绝对不用注释;只有当“为什么这样做”只能靠语言表达的时候,才动用注释。而且这样的注释,我会尽量把有效期限写出来。比如“该兼容逻辑只在旧版本客户端存在期间有效,旧版本下线后可以删除”,这句话比单纯写“这里做兼容”有价值得多,因为它给了后人一个明确的清理信号。5.3 评审注重的四个检查项最后分享一下我在评审代码时,关于注释和格式化的固定检查清单。第一,注释是不是在复述代码做了什么。是的话,要求删掉。第二,注释里有没有“为什么”。没有的话,看看是否需要补一条。第三,注释和代码是否一致。不一致的话,要求当场解决,而不是留到以后。第四,格式化工具是否已经运行。没有运行的话,让提交者先跑完再提交。这四条检查项执行久了,团队会慢慢形成默契:注释是稀缺资源,只留给重要信息;格式化是自动流程,不需要人操心。等到这两种状态都成为日常,我发现自己写代码的心态也变了——我不再需要费心维护那些视觉上的整齐,也不用时刻担心一行注释会不会误导三个月后的自己。注意力被彻底解放出来,放在真正需要人做判断的事情上:这个模块边界是否清晰,这段逻辑是否真的做了它该做的事。格式化工具用起来可能只需要半天,但把“注释要克制、格式要自动”这两条刻进习惯,值得花很长时间反复练。每次代码评审都是一次训练,每次重构都是一次验证。慢慢地你会发现,干净代码不是靠某一招实现的,而是这些看起来不起眼的小事,一件一件累积出来的。