
“你六的太狠了牛批我真是躲得过初一躲不过15啊”这句话这段时间经常出现在技术群的聊天记录里。第一次看到时我还以为谁又在玩什么谐音梗。后来才反应过来这是程序员之间一个精准得有点残忍的日常总结某个临时方案当时确实让你躲过了上线检查、验收测试、或者当天就要交付的 deadline你以为这关过了就没事了。结果到了第十五天它在一行日志里、一次重启之后、或者服务流量突然上来的时候把之前所有的“侥幸”一次性连本带利地追了回来。说“躲不过 15”其实重点不是那个“15”具体指哪一天。它真正想说的是在软件工程里任何靠巧合、靠记忆、靠“先跑通再说”的方式掩盖起来的问题都不会因为你的祈祷而消失。它只是在你最没有防备的时候换了副面孔重新出现。这篇文章不想顺着段子继续玩下去。我想把它当成一个真实工程问题的入口来聊为什么我们总在“躲初一”躲过之后那个“十五”到底藏在哪里以及一个更重要的——从一个习惯性补救的人变成一个习惯性预防的人中间到底需要补上哪些具体能力。1. 为什么“躲过初一”这件事本身就是最大的风险信号先说一个我自己的判断在大部分真实项目里真正造成线上事故的常常不是一开始就写错的复杂逻辑而是某个“临时躲一下”的小改动。1.1 临时方案的本质是“推迟决策”你回想一下最近那次临时改动是怎么发生的。大概率是这样一个流程版本计划排好了测试也在跑突然发现一个边界情况没处理。直接重构时间不够。完整补测试流程太长。于是选择了最省事的那条路——加个 if 分支注释掉一小段代码或者把一个常量改成看起来“差不多够用”的值。为了让发布能过你在代码旁边加了一行注释“临时处理后续再优化。”这行注释就是“初一”的仪式感。你明知道这不是长期方案但当下最紧迫的目标是让流程往前走。这个逻辑在单次场景里没有错它错在后续没有人真正把它“后续”掉。从工程角度看临时方案本质上不是一个技术决策而是一个延期决策。你把“什么时候彻底解决这个问题”的选择权从一个自己可控的时间点推迟到了一个完全未知的触发条件上。而这个触发条件不会提前通知你。1.2 为什么反馈总是来得“恰逢其时”有经验的人会告诉你这种“十五”最可怕的一点是它总会在你最忙的时候出现。听起来像玄学但从系统运行规律来看这完全可以用概率解释。临时方案大概率跳过了一些正常流程没有补充边界测试、没有写清楚异常处理、没有更新相关文档。这意味着它的稳定运行依赖的是一系列恰好成立的外部条件。比如恰好没有人改动那个方法、恰好数据格式没变、恰好并发量还在预期范围内。在条件没有变化的时候它表现得像一棵无毒无害的小草。一旦任何依赖条件产生偏移——上游接口加了字段、配置中心换了一台机器、某个定时任务延迟了 200 毫秒——“恰好成立”就会变成“恰好不成立”。出事时间越晚越说明这个方案在真实环境里存活了足够久被足够多的流量“检查”过。等到它真的坏了通常不是坏在一个小问题上而是坏在条件偏移累积到某个临界点的时候。这就是为什么“十五”总是给人感觉“防不胜防”。1.3 要警惕的不是哪一次失误而是“每次都能躲过”的侥幸我觉得真正值得警惕的反而不是某一次事故而是你开始习惯“躲过去”这个动作本身。一旦尝到了“临时改一下就能过”的甜头人的决策模式会慢慢变化遇到问题第一反应不再去想“怎么彻底解决”而是想“怎么最快绕过去”。这种心态在个人项目里危害还不算大因为你自己知道哪些地方埋着雷。但一旦放进团队协作、多人维护、长期迭代的环境里每一个“绕过去”都会留下一段别人看不懂的隐形成本。改代码的人以为自己在解决业务问题实际上是在给整个系统的未来埋了一张张没有期限的欠条。2. 那些“躲不过的十五”最常见的四种隐藏形式“十五”不是某一行特定代码而是临时方案长期存在的几种典型形态。我们把它们拆开看确认很多事故背后都有相同的基本盘。2.1 被注释掉的“历史包袱”项目里最常见的“十五”是一大段被注释掉的代码。它可能是一个没人记得为什么废弃的函数、一段调不通的第三方请求逻辑、或者一个被替换掉的旧算法。注释掉它的那个人当时可能是想“先留着万一以后要用呢”。问题是一旦代码进入版本管理历史版本早就替你保管好了一切“注释保留”根本没有必要。被注释代码最危险的地方是它给了后来者一个错误的信号这段逻辑曾经是关键的现在虽然没用了但可能还有价值。于是有人试图恢复它有人试图在里面找可复用的片段还有人怕删掉之后出问题就不敢动。一个本来应该清爽的文件逐渐变成了考古现场。真正到最后引起冲突的往往是某次重构时工具自动格式化了注释内容或者恢复了一段早已不兼容的旧逻辑。2.2 靠“补丁套补丁”维持的脆弱结构如果说注释代码是静态的隐患那么补丁套补丁就是动态的系统性风险。一开始可能只是一处边界判断没写好于是你在调用处加了一个 if把特殊场景挡回去。过了一阵子新需求又把原来的假设打破了于是你又加了一个状态位这次如果需要同时兼容旧状态和新状态就再多套一层逻辑。每一次小修复单独拿出来看都成立没有明显错误。但把五六个这样的修复叠在一起之后整个分支结构已经复杂到没有人能一眼讲清楚“什么条件下走哪条路”。这种结构真正的问题不是“丑”而是测试覆盖跟不上。你修补的都是特定场景写测试的时候也只针对当时的输入。可当输入组合出现你从未想到过的情况时整个分支会走出一条谁都无法预期的执行路径。到那时你再回头去找是哪一处补丁引发的错误排查成本会高到让人崩溃。2.3 永远在“下周”的异常处理很多团队都有一种心照不宣的逃避只处理主流程不处理异常分支。不是说完全没有 try-catch而是 catch 住之后只打一行日志或者直接 return null根本不考虑上层拿到 null 之后会怎样。这种代码看起来“稳”因为它在异常来临时不会崩溃。但它把崩溃延迟到了更高的调用层由另一个毫不知情的模块去承担。等到“十五”出现时日志里看到的是某个接口返回了空对象但真正的根源却在几层调用以下。你要顺着调用链一点点追溯才发现那个空对象是两个月前某人为了“先让主流程跑通”而留下的默认值。异常处理看起来最不重要实际上它决定了系统在极端情况下的行为。真正负责的写法不光要想“正常时怎么走”还要想“异常时给调用方一个什么样的结果才不至于让问题以更隐蔽的方式扩散”。2.4 没有更新状态的“流程跳过”还有一种很难用代码审查发现的“十五”是流程层面上的跳过。比如某个配置文件的修改只在本机生效没提交到仓库。比如某个接口新增了鉴权字段A 服务更新了B 服务还在用老格式。再比如一次发布手册里要求做数据校验但因为“之前都正常”就顺理成章地跳过了。这种问题不会立刻产生错误甚至会在测试环境完全无法复现。它只在发布到特定环境、特定流量比例下才突然冒出来。这也是我认为最需要警惕的一类。因为它不靠代码质量取胜而靠流程约束取胜。个人开发时还能靠记忆力维持一旦到了团队协作任何“口头确认”“大家心里有数”的环节都是潜在的失控点。3. 别再靠“赌运气”过关一套可落地的检查顺序前面分析了这么多核心是想说明“躲得过初一”不是运气问题而是检查机制不够具体的问题。如果我们的目标是让问题在早期自己暴露出来而不是在线上被用户发现那就要把“检查”从一句口号变成一套可执行的顺序。我在自己的工程实践里总结了一个比较顺手的检查框架叫“三层过滤法”。它不复杂但能有效覆盖大多数“躲初一行为”的盲区。3.1 第一层改动发生前先回答自己三个问题很多“临时方案”之所以后来变成了事故是因为改动发生的那一刻思考是停下来的。手比脑子快一看到问题就立刻搜代码、改逻辑、提交根本没有给“为什么要改”留出反应时间。在动手前先强迫自己回答三个问题这个改动到底是为了让什么场景变得正确如果不做这个改动系统现在会发生什么这个改动的副作用会影响哪些既有功能这三个问题回答不上来任何一个就说明你对这个改动的理解还不足以支撑你把它提交进代码库。同理如果在改动的时候写不清“为什么”那这个改动大概率也不是一个清晰的改动。3.2 第二层从输入、环境、依赖三个方向做快速验证单次能跑通不等于在所有环境都能跑通。在提交前按三个方向快速过一遍输入方向数据格式、字段为空、编码类型、超长内容、极端数值。如果输入结构发生变化代码能不能给出合理反馈环境方向本机、测试环境、生产环境之间配置、路径、系统用户、文件权限、端口是否一致有没有只在本机成立的硬编码依赖方向第三方服务是否可用、版本是否匹配、调用是否设置了合理的超时和重试策略、上游接口返回结构是否有兼容处理这个过程不需要写自动化测试只需要在脑子里快速过一遍。但如果你发现其中任何一个方向需要“假设它没问题”才能通过那恰恰说明这里就是最可能出问题的角落。3.3 第三层上线后 24 小时内主动去“找”问题等到问题被用户发现就已经晚了。真正有效的习惯是在上线后的极短时间窗口内自己先去怀疑系统。我一般会做三件事盯关键日志看有没有新增的 WARN 和 ERROR。跑一遍主要链路的主流程确认核心功能没有被影响。用一两条边界用例专门去触发那个“临时方案”所覆盖的场景确认行为符合预期。这个 24 小时窗口非常宝贵。问题刚出现时上下文还新鲜代码还写得动。等过了一个星期再回头看连你自己都不一定记得当初为什么那么写了。注意上线后的“主动排查”不是为了证明代码没问题而是为了证明自己还没来得及制造问题。哪怕日志一切正常也要警惕“暂时一切正常”和“真的正确”之间的区别。4. 一个临时代码的完整治理框架如果你手头已经积压了不少“躲过初一”的改动现在最需要做的不是懊恼而是给它们挂上号、排好期、逐一消化。4.1 给临时改动建立“技术债台账”我见过不少项目代码里散落着各种 TODO、FIXME、临时处理但没有人清楚这些标记到底对应多少工作量。真正要治理先把它们全部收集起来放进一个表格里。这个表格不需要很复杂通常包含以下几列字段作用示例临时改动位置文件、函数、行号方便快速定位order_service.py L128临时原因为什么当时选择绕开上游接口还没就绪先用默认值影响范围这个改动会牵连哪些模块下单主流程、订单导出触发条件什么情况下会暴露问题上游新版本上线后字段为空负责人当前最了解这块逻辑的人小王到期日期计划彻底下限的日期下个版本迭代正式方案替代临时改动的方案是什么接上游新字段去掉默认值状态待处理 / 处理中 / 已下线处理中这个台账的价值在于它把“欠债”从一种情绪压力变成了可见的任务列表。你不需要一下子全部还清但每个月至少处理一两项。只要状态列里有“已下线”就说明这套机制在起作用。4.2 一次只处理一个“十五”不要顺手重构一切很多人面对一堆历史债容易犯一个毛病好不容易决定清理了就想把相关问题一次性全部理顺。结果往往是越改越大最后连正常功能都被影响了。更稳的做法是挑一个影响范围最明确、触发条件最清晰的“十五”单独处理。处理完只跑相关模块的回归测试确认行为没有变化再把它从台账里划掉。不要顺手改掉旁边的代码结构不要在同一个提交里既修 bug 又重构。任何和当前任务无关的优化都单独记录再安排时间。4.3 每次十二指肠式的“躲”都值得复盘一次如果一次事故里你发现自己或同事使用了“临时方案”不要只把代码修掉就结束。多问一句“为什么会走到用临时方案这一步”有时候比修代码本身更有价值。常见的原因包括需求没有讲清楚边界、排期没有留出测试时间、依赖接口的变更没有提前同步、代码 review 流于形式。这些问题单独看都不是大问题但一旦反复出现就会迫使每个人习惯性地“先躲为敬”。5. 从“躲过十五”到“让问题无处可藏”把话题从技术细节拉回到工作方式上。我觉得“躲得过初一躲不过十五”这句流行语本质上是在描述一种被动式的工作习惯。一个被动工作的开发者总是在等系统用事故来告诉他哪里有问题。这种方式的成本太高反馈周期太长而且每次事故的代价都可能超出预期。反过来看主动工程能力的核心不是“不会出问题”而是“问题出现得更早、更小、更好定位”。要做到这一点靠的不是某个高深的算法也不是某套炫酷的架构而是一堆小到容易被忽略的习惯每次提交前先看一遍 diff 里有没有“临时”“稍后”“随便”这样的字样。每次写完异常分支问自己一句如果这里出错了日志能让人一眼看出原因吗每次发现一段没人能解释的注释代码不是去猜而是去看版本历史找到它的出生原因。每次流程跳过之前先明确一点这次跳过靠什么来保证不会出问题这些习惯单独拿出来都不难难的是不让“赶进度”带跑自己的判断。长期来看一个系统的健康程度不是由技术水平最高的那个开发者决定的而是由所有人默认接受的最低代码标准决定的。如果你想让自己所在的项目不再经常“躲初一”最有效的一步就是把那个最低标准往上抬一点点。“十五”终究会来。但如果我们提前把该检查的检查了、该记录的记录了、该修复的计划排好了那它来的时候就不再是事故而只是一次普通的功能迭代。下次再有人感慨“躲得过初一躲不过 15”时你至少可以有自己的答案初一不该靠躲十五也不是要靠运气去赌。真正值得练的能力是让问题在它很小的时候就无处可藏。