凌晨两点十七分,我盯着屏幕上一段三百行的员修try-catch嵌套,感觉自己的管动大脑和这段代码一样,正在陷入某种无限递归的程序僵局。旁边的员修咖啡早就凉透了,杯壁上结着一圈深褐色的管动垢。那个bug的程序表现很诡异——线上环境偶发,本地永远复现不了。员修日志里只有一行孤零零的管动NullPointerException,堆栈信息像被狗啃过一样,程序缺了最关键的员修那几帧。
这时候我想起那个流传很广的管动动图:一个程序员蹲在墙角,面前是程序一根爆裂的水管,水柱喷得到处都是员修。他手里攥着扳手,管动表情从焦虑变成麻木,最后干脆把扳手往地上一扔,整个人趴在漏水口上,用身体堵住。水暂时不喷了,但你能看到他背后的衣服正在慢慢渗湿。
——那个动图的静态还原。你看,连ASCII画都透着一股手忙脚乱。
这个画面之所以让每个程序员都心头一紧,因为它太准了。修bug的本质,很多时候不是找到那个“正确的修复方案”,而是用最快速度让报错消失。产品经理在群里@你,运维在监控面板上画红圈,老板路过你工位时脚步放慢了三秒。压力从四面八方涌来,你根本没时间去做根因分析。先堵上,先让水别喷了,至于以后会不会从别的地方渗出来——那是未来的你该头疼的事。
我见过最典型的一个“贴胶带式修复”。某个电商项目,购物车金额计算偶尔会少算一分钱。排查了半天,发现是浮点数精度问题。正确的做法是把金额转成整数分再做运算。但当时大促在即,改核心计算逻辑风险太高。于是有人加了一行:
if (Math.abs(expected - actual) < 0.02) {
actual = expected;
}
这行代码上线后,bug确实不报了。但三个月后,财务对账时发现有一批订单的总金额和明细对不上,差了整整十七块三毛。原因是有几笔订单同时触发了这个补丁,把原本正确的金额也“修正”成了错误的值。你看,水从爆裂口不喷了,但它顺着墙缝流到了楼下邻居家。
还有一种更隐蔽的“堵水管”——catch块里什么都不写,或者只打一行日志然后继续跑。表面上看程序不崩溃了,用户体验丝滑流畅。实际上错误数据正在数据库里悄悄堆积,像水管里的水垢,越积越厚,直到某天整个系统突然罢工。那时候你打开日志,发现过去半年里每天都有几十条ERROR,只是没人看。
我并不是说所有临时修复都是错的。有时候业务就是需要先止血,再慢慢缝合。但问题在于,很多人把“暂时堵住”当成了“已经修好”。那个动图最扎心的地方在于,程序员用身体堵住漏水口之后,画面就停在那里了。没有后续,没有他站起来去找总阀门,没有他打电话叫专业水管工。他就那么趴着,水在背后蔓延。
真实世界里,我见过一个团队的做法值得借鉴。他们在代码里加了一个特殊的注释标记:
// 到期日:2025-06-30
然后写了个脚本,每天扫描代码库,把过期的FIXME-TEMP列出来发到群里。这就像在水管旁边放了个闹钟,提醒你:哥们儿,你还在用身体堵着呢,该去找总阀门了。
说到底,修bug和修水管一样,真正的技术不在于堵漏的速度,而在于你愿不愿意在浑身湿透之后,还爬起来去关掉那个总闸。动图里的程序员如果一直趴着,迟早会被水淹没。而现实中的我们,至少可以做到一件事——在贴胶带的时候,在旁边写清楚这胶带是什么时候贴的,以及它什么时候该被撕掉。
凌晨三点零四分,我终于在某个第三方库的源码里找到了那个吞掉异常的地方。一行catch (Exception e) { },空得理直气壮。我把它改成打印堆栈,重新跑了一遍测试。屏幕上的红色消失了。我盯着那行改完的代码,突然觉得它就像一张刚贴上去的创可贴——暂时止住了血,但伤口还在下面。
我把咖啡杯扔进垃圾桶,决定明天再来看它。
```