我先说个真事。前几天我们组一个小伙子把一段接口返回的字段统一成了下划线转驼峰,每个布尔变量都起了isXxx这种一眼能看懂的名字,原来二十多行的 if 嵌套也被他拆成了三个独立小函数。结果评审一过,邻组同事居然专门跑来问他要代码片段当参考,连测试同事都多看了两眼他的提测说明。标题说“同事纷纷上门祝贺”,真没夸张。
这件事我琢磨了几天,发现大家夸他代码好,绕来绕去就几个字:省心。今天就把这种“省心代码”怎么练出来的拆一遍。
1. 先想清楚:同事眼里的“好代码”到底长什么样
1.1 能跑只是底线,能改才是本事
很多程序员入行时以为“写得好”指算法够妙、API 冷门、一行代码把功能算完。但真在团队里待过的人都知道,同事不会因为你用了位运算、lambda 嵌套怪招来给你鼓掌,真正让他们上门的理由,往往是你的代码让人少猜了十分钟。
代码要同时给机器和同事看。机器看语法,同事看语义。如果一段代码机器能跑,但是别人接手时完全不知道哪一步在干什么,那它只是“写出来了”,离“写好了”还有差距。换句话说,写完代码以后,你能闭上眼睛把数据流转讲一遍,讲得清楚,代码基本就及格了;讲不清楚的地方,就是留给大家踩坑的地方。
踩过的坑多了就会发现,团队里最受欢迎的人,不是技术名词说得最多的那个人,而是交出来的代码让别人接手成本最低的那个人。所谓功底好,表现在结果上,就是“别人愿意动你的代码”。
1.2 三个硬指标:可读、可查、可拆
我给“好代码”定了三个特别朴素的指标,不是学术定义,就是日常评审的实用标准。
第一是可读。拿到一段代码,不需要来回翻上下文,就能大致知道它处理了什么、先做什么后做什么。实现手段主要靠命名和结构,而不是注释。第二个是可查。线上或者测试环境出了问题,能从日志和报错信息比较快地定位到对应代码位置。这就要求函数职责分离,一个函数干一件事,查起来才不会像大海捞针。第三个是可拆。改需求时,能在局部调整,而不是牵一发动全身。模块边界清楚、依赖方向明确,才能拆得开。
这三个指标不需要什么高深理论,但对开发习惯要求不低。养成好习惯前,先得把“写完运行通过”这种思维升级成“写完后同事能在三分钟内看懂”。
1.3 你的同事里有一个叫“未来的自己”
还有一种同事最容易被忽略,就是三个月后的自己。当时清爽的代码,三个月后回来看,也可能一脸懵。我有过这种经历:为了赶版本写了一段非常“聪明”的缓存刷新逻辑,当时觉得天下无敌,后来线上问题定位,自己都要在草稿纸上画半天才想起来意图。
后来我就给自己留了个规矩:凡是当时想了一会儿才想明白的地方,必须留一句注释说明思路,哪怕只是半行。这句话不是写给别人的,是写给未来我的。“好代码”的时间维度也要拉长,不是上线那一刻算结束,而是维护周期里一直能被人理解。
2. 命名与结构:第一个让同事点头的细节
2.1 变量命名不是玄学,是信息传递
命名是代码评审里最容易被看到的部分。变量名这件事,我常用的判断标准是:把代码里所有变量名换成a、b、tmp,再看一遍,如果仍然能猜到大致逻辑,说明结构还行;如果完全看不懂,那问题通常不在名字,而在变量太多、函数太长。
举一个真实场景。我负责过一个订单模块,早期代码里有大量flag开头的写法:
user = get_user() flag = user.is_vip if flag: fee = calc_vip_price() else: fee = calc_normal_price()这段代码本身没错,但flag完全没表达业务含义。后来改成了is_vip_member,一眼就能看出判断的是会员状态。同样的逻辑,信息量差得非常远。再比如快速排序里常见的双指针,教材里爱写i和j,但换到工程代码里,写成left和right就不用在脑子里反复映射。
# 推荐:left/right 是上下文自明的 def quicksort_range(arr, lo, hi): if lo >= hi: return left, right = lo, hi pivot = arr[(lo + hi) // 2] while left <= right: while arr[left] < pivot: left += 1 while arr[right] > pivot: right -= 1 if left <= right: arr[left], arr[right] = arr[right], arr[left] left += 1 right -= 1 quicksort_range(arr, lo, right) quicksort_range(arr, left, hi)临时变量不是不能用,但要有“生命周期意识”:如果一个变量的作用范围超过五六行,最好给它一个能讲清楚用途的名字。给同事省下的每一次思考,最后都会变成你代码口碑的积累。
2.2 函数边界:一个函数最好只说一件事
“一个函数只干一件事”这句话听上去像废话,真正写起来最难。一种常见情况是,开发时需求明确,顺手把校验、鉴权、业务计算、日志、通知全塞进一个handleOrder(),开始还行,等功能一多,这个函数会膨胀到几百行。
我会先把大函数拆开,拆法的核心不是“少写几行”,而是“让每个片段有名字”。比如一个下单服务,可以拆成checkOrder、lockStock、calcPayment、flushEvent这几步,每一步对应一个函数名。主流程读下来,相当于在看一张流程清单:
def handle_order(order_id): check_order(order_id) lock_stock(order_id) fee = calc_payment(order_id) flush_event(order_id, fee)这样拆完之后,挂在哪一步,就去哪个函数里看,不需要从头到尾读一遍所有实现细节。同事看到这种主流程,通常都能很快进入状态,这也是“上门祝贺”的关键原因之一:他们不用花半小时才能看懂你要干什么。
一个很实用的经验是:如果函数超过 30 行,就逼自己写一行注释说明函数干了什么。写不出来或者要写两行才能说清楚,那它逻辑上很可能不是一个动作。
2.3 风格统一:把“看不顺眼”从评审里拿掉
团队里经常有这种吵架:有人喜欢双引号,有人喜欢单引号,有人缩进两格,有人缩进四格。争这个没有意义,因为它是纯偏好问题。解决办法很简单,上一个自动格式化的工具,比如 Python 的 Black、JavaScript 的 Prettier、Go 的 gofmt,让工具来做决定,然后众人闭嘴。
格式化工具的好处不只是漂亮。它最大的价值是把 diff 变小,代码评审时只看到业务改动,不会因为某个人顺手把别的文件格式改了一遍,导致一堆无关变动混进来。这个是很多新人没意识到的点:一个几百行的格式化 diff,足以让评审同事失去耐心。
我在团队里的要求是,提交前必须跑一次格式化,本地配置好保存自动格式化,最好在提交钩子里也挂上。宁可让工具花半秒钟整理一下,也不要让同事在评审页面上满屏找改动点。
3. 注释与文档:别让名字出现在“返工写注释”名单上
3.1 注释是给“半年后的同事”写的,不是给编译器写的
很多人对注释的态度是两个极端:要么一点不写,要么写满整屏。这两种都不可取。注释的读者是人,而且多半是对这段代码缺少上下文的后来人。你要解决的是“他为什么看不懂”,而不是“把代码翻译成中文”。
最近那个段子说“公司要求前程序员回公司写注释”,听起来好笑,背后是血泪:项目上线后没人知道那段正则表达式匹配的是什么,也没人敢动那几百行无人区代码。与其等被资源遣返,不如在写的时候顺手留两句。一个补丁的注释成本可能只有一分钟,但能避免后来人几天的大冒险。
3.2 什么位置值得写注释?
根据我的经验,下面几类是性价比最高的注释位置。
第一类是业务规则复杂的计算。比如订单满减、库存扣减、优惠叠加,这种逻辑靠看代码推理太慢,注释直接写明规则来源和边界情况,能救很多人命。第二类是特殊决策点。为什么用消息队列而不是直接调接口,为什么这里要 sleep 三秒再重试,这类“为什么”是代码本身回答不了的。第三类是一个函数的入口文档。对外被多处调用的函数,至少要写清楚参数、返回值和抛出异常。
举个例子,一个查询订单费用的函数,如果有这么一段 docstring,接手的人会非常舒服:
def calc_shop_fee(order: Order, use_coupon: bool = True) -> Decimal: """计算订单实付金额。 规则: 1. 平台券和店铺券互斥,取优惠力度更大的那张; 2. 若订单命中满减活动,满减金额在券后计算; 3. 结果保留两位小数,调用方不要自行四舍五入。 Raises: OrderCalcError: 订单金额状态非法或优惠金额超出订单金额。 """这种注释没有一句废话,也没有复述代码,它补充的是上下文和约束。同事拿到手就知道该怎么调用、该注意什么。
3.3 注释不是越多越好,也有垃圾注释
我见过不少代码,注释全是这种:
# 设置用户姓名 user.name = name这就是典型的“替代编译器阅读”式注释,没有任何新信息。垃圾注释的危害在于它会占据注意力,真正有价值的注释会被淹没在里面。所以最重要的不是写多少,而是挑对位置。凡是“为什么”,值得写;凡是“是什么”,代码自己会说话。
另外要养成习惯:改逻辑的时候同步改注释,否则注释反而是误导。有一次我看到一行注释说“暂存用户地址”,实际上代码已经改成从配置中心读取地址,这种过期注释比没有注释还坑人。后来我就规定,注释跟着需求走,需求变了注释必须一起更新。
4. 能少吵十次架的工程链:格式化、静态检查与代码补全
4.1 格式化工具:让“风格问题”永远退出评审
前面在风格统一里已经说了格式化工具的重要性,这里再往深讲一层。真正的工程团队,不是靠纪律或者约定维持统一风格,而是靠自动化的钩子。比如提交前执行pre-commit钩子跑一遍格式化,不通过就禁止提交。这样一来,任何人的代码进入仓库时都是同一个风格,你在评审里再也不用看到“这行怎么多了一个空格”这种评论。
我个人配置过一个前端项目,用了 Prettier 加 ESLint。效果非常明显:以前 Reviewer 经常因为引号、分号、缩进问题打口水战,配置之后这类讨论彻底消失。团队时间被大量解放出来,讨论逻辑和设计,而不是在标点符号上。
4.2 静态检查:把低级错误挡在同事看到之前
除了格式化,静态检查工具是第二道保险。现在各语言都有比较成熟的方案:Python 有 Ruff、Flake8、pylint,JavaScript 有 ESLint,Java 有 Checkstyle/SpotBugs,C/C++ 有 cppcheck。它们的价值不单是检查风格,还会发现一些潜在的 bug,比如变量未定义、类型问题、明显的死代码、逻辑冲突。
我每次提交前都会先跑一遍本地静态检查,所有报错都清零再发 Merge Request。这么做的好处是,CI 不会因为低级问题挂掉,同事也不会在评审时揪着“这里有个拼写错误”不放。整体来看,静态检查是防线上移,越早发现问题成本越低。
4.3 代码补全时代,人的判断更值钱
最近两年 AI 代码补全工具用得挺多,大家误以为“代码写得好”这件事被工具接管了。其实相反,工具越强,人的判断越重要。我见过有人让 AI 生成了一大段看起来很全但根本不符合团队规范的代码,反而让评审同事花更多时间去纠偏。
用 AI 补全的正确姿势,是把它当成“快一点的联想输入”,而不是“需求翻译机”。先生成命名良好的函数骨架,由人来确认边界和业务规则,再让补全去填充局部实现。关键的业务逻辑、权限校验、支付计算,我一般不会让 AI 直接写完整,因为一旦出错,责任还在人身上。同事真正欣赏的,是那种能分清楚“工具能帮什么、人需要负责什么”的工程师。
5. 让协作记录也“闪闪发光”:提交信息、分支管理与 Code Review 话术
5.1 提交信息是把改动讲给未来的同事
很多人觉得git commit -m "update"已经够了,但等你想在 git log 里找一段历史时,会非常痛苦。好的提交信息应当像日记,能让人不用看代码就知道这次改了什么以及为什么改。
一个我常用的模板是:
feat(order): 增加店铺券与平台券互斥逻辑 店铺券和平台券不能叠加使用,取优惠金额最大的一张。 补充了 order_calc_test 中的边界用例,本地全量测试通过。第一行是类型加简述,类型用feat表示新功能,fix表示修 bug,docs表示文档,refactor表示重构。主体部分解释“为什么”和“怎么验证”。别小看这两行字,几个月后排查问题,git log 里能直接看到这段描述,会省下大量考古时间。
5.2 分支策略:小步提交,别让 diff 变成灾难
另一个容易让同事“叹气”的场景,是一个 Merge Request 里塞了几百个文件、几十个提交,里面还混着依赖升级、配置文件改动和业务代码。这种 MR 别说评审了,连 diff 都看不下去。
我的经验是:一个 MR 只解决一个需求或者一个 bug。每次提交尽量小一点,比如“增加一个接口字段”“修复一个空指针”,而不是攒了一周的工作一次性上来。这样既能减少冲突,也方便回溯。如果代码仓库是 Gitee 或者 GitHub,还要注意.gitignore把node_modules、dist、.env、密钥文件这些排除掉,不要把本地垃圾和敏感信息传到仓库里。曾经有人把数据库密码提交进 Git 历史,后面清记录非常麻烦,这个坑希望大家都不要再踩。
5.3 Code Review:用提问代替指责,效果翻倍
代码好不好,最终要过评审这一关。很多开发者在评审时习惯直接说“这样不对”,但对方听起来就像在被人审视。我比较推荐的说法是:“这块逻辑我有点绕,能不能解释一下为什么用这个方案?”“这里要不要抽个函数,看起来更直观?”用提问的方式帮对方自己意识到问题,接受度会高很多。
反过来,如果你是被评审的那一个,也要明白评审不是找茬。有人质疑你的写法,先别急着反驳,把上下文和取舍讲清楚。如果对方建议合理,就大方接受;如果不合理,拿出业务规则说明白。一个成熟工程师的标志,就是能把技术争论停留在代码里,不上升到人身。这样几个版本之后,大家就知道你的代码值得细看,也愿意来帮你 review,这本身就是一种“祝贺”。
6. 实战中的典型翻车现场和排查技巧
6.1 排查问题:先复现、再改码,别让同事替你探险
我处理过一个线上问题,用户的请求偶尔超时,后来发现 Nginx 的 worker 进程 CPU 突然跑满。当时第一反应不是改代码,而是用top找到 CPU 高的进程,再strace -p <pid>看它在忙什么,最后查 access log 发现某个接口参数异常导致缓存穿透,回到业务代码里定位到了一个死循环的遍历逻辑。
这个过程完全可以写进提交说明和评审记录,让同事知道排查思路是什么。真正的“好代码”不只是静态的可读,还包括动态的可诊断。你留下日志、链路追踪 ID、错误码,同事遇到线上问题就能顺着线索查下去,不至于全组人一起抓瞎。
6.2 重构:先补测试,再动剪刀
碰过乱代码,谁都有“推倒重来”的冲动。我的经验是,重构一定不能裸奔。先把当前行为用测试固定下来,再开始拆分和重命名。哪怕是个小函数,也先写两三个用例把它钉住,再动手。重构完成后跑测试,绿了才算安全。
重构还有个技巧:一次只做一类动作。要么只改命名,要么只拆函数,要么只调整目录。把它们揉在一起,出问题很难定位。小步骤前进,每步都有测试护航,这种节奏看起来很慢,但实际交付速度反而快,因为你不需要反复救火。
6.3 常见问题速查表
| 问题 | 典型表现 | 排查思路 | 预防方法 |
|---|---|---|---|
| 命名模糊 | 变量名叫 a、b、tmp、flag | 全局搜索临时变量名数量 | 评审时把命名列为必查项 |
| 注释缺失 | 业务逻辑只能靠猜 | 让新同事读一遍代码计时 | 复杂规则写 why |
| 注释废话 | 注释和代码逐行重复 | 注释改为只保留有增量信息的部分 | 写注释前问一句“代码自己能说明吗” |
| 大面积改动 | MR diff 几百个文件 | 审查改动范围是否单一 | 小步提交,MR 聚焦 |
| 提交信息空洞 | git log 全是 update | 查看 git log 的阅读成本 | 按 type + summary 写 |
| 静态检查不过 | CI 红灯在格式或类型上报错 | 本地先跑 lint 和 test | 提交钩子强制检查 |
| 风格争执 | 评审争论引号缩进 | 配置统一工具后禁止偏好评判 | 格式化工具自动处理 |
6.4 想被同事“祝贺”,从今晚的提交开始
这东西不需要等一个大会战。手头随便找个函数,把三个变量名改清楚,拆一个五十行的大块头,补上两行关键注释,提交信息写完整,明天同事打开你的 MR 就会觉得变舒服了。我见过最让人刮目相看的工程师,不是突然写了个炫酷模块,而是连续三个月每份提交都保持这种水准,口碑慢慢就出来了。代码写得清楚,不一定会被当众表扬,但一定会在每个关键节点帮你积累信任。等哪天邻组同事带着问题来敲你的工位,你就知道,那种感觉确实跟“上门祝贺”差不多。