跳到主要内容

那个没写 default 的 switch,比 AI 贵多了

原石
原石

· 阅读约 5 分钟

先把代码贴上。

func fold(events []Event) Result {
    var r Result
    for _, e := range events {
        switch e.Type {
        case "created":
            r = applyCreated(r, e)
        case "validated":
            r = applyValidated(r, e)
        case "completed":
            r = applyCompleted(r, e)
        }
    }
    return r
}

没有 default。没匹配上的事件类型,什么都不发生,循环继续。review 的时候没人会为它写一句评论,它看上去太干净了。

然后生产里,1482 条裁决记录中有一条的键写成了 outcome:pass,不是校验器认的那个词。schema 看它是个合法字符串,放行。这个 switch 也没匹配上,跳过。于是它被归进了默认那一侧——失败侧。没人报错,没人知道词汇对不上。直到某天有人拿这个聚合出来的数字去开了个会。

我想说的不是"AI 让验证变贵了"这个结论。那篇文章我看了,账算得对:实现成本一次性,验证成本经常性。但评论区大家全会的那句药方是"让非法状态无法表示"。

这句我认。可它有一条看不见的边界。

约束挡的是你想到的

生产里最贵的那些故障,很大一部分状态是合法的。

有人写一条模糊匹配规则归一化公司名,三个小时写完。之后两周用来排查召回率悄悄掉的 8 个点。没有哪个状态是非法的——记录全过 schema,转换全合法。出问题的是管道没法回放变更前的状态,你没法证明"改动前能召回的,改完还能召回"。缺口不是"非法状态",是缺一条时间线。约束补不上它。

再一个。发帖任务对文本做 300 字符上限检查。

if len(post.Body) > 300 { reject(post) }
// 真正发出去的是这个
send(post.Body + "&utm_source=track-1")

限制是对的,检查也真在跑。只是它检查存储里的文本,超长的是发送前的文本。同一份约束,挂在错误的层上,等于没挂。我手上那个小东西踩过更离谱的:约束写在一段早就没人走的路径上,跑了半年没人发现它其实没跑。

所以问题不止"有没有约束"。还有两件更细的:约束拦的那个对象,和实际出问题的那个对象,是不是同一个;以及,系统碰到一个它没预料到的输入,是默认放行,还是默认炸。

第二条才是分水岭。

默认放行,等于把未知折算成合法

一个约束带着默认放行的兜底,你想的是"我只允许这几种",代码做的是"我不认识的都当没事"。

default:
    return Result{}, fmt.Errorf("unknown event type %q, seq %d", e.Type, e.Seq)
}

一行。区别是前者带着一个错的数一路往下,让你在季度会上才发现不对;后者在源头炸,还告诉你炸在哪条、哪个位置。

聚合要能大声失败。这算老生常谈,也正因为它要你动脑子回答"这里到底该发生什么",所以成了最常被跳过的一行。跳过它最省事。

说句题外话,验证是两笔成本

这个我最近才想明白。

一笔是跑检查的成本——测试、类型、约束、CI。另一笔是"你有理由去相信这个检查结果"的成本。

第一笔,代理很擅长,写得又快又多。第二笔,代理让它更糟。

因为最省事的做法,是把实现和测试交给同一个头脑。代理写实现,代理写测试,两边用的是同一套理解。测试绿了。但你得问:那个绿说明了什么。

它说明的只是自洽,不是正确。同一个理解长出来的实现和测试,互相印证不携带任何信息——它理解错的地方,实现错,测试也错,两边严丝合缝地一起错。你拿到一个好看的绿,花出去的成本是零,拿回来的信息也是零。

让模型给它的实现补断言,等于让它给自己的 bug 当裁判。它可以,测试会过,但你不能因此少想一件事。我的做法是:函体让它写,断言我自己写。断什么、边界在哪、什么算对,这部分不外包。不是不信它的能力——那第二笔成本本来就没法外包,"有理由相信"这个理由得长在我自己脑子里。

那笔没人记的账

那篇里还有个 10 万对 12 万的账:A 便宜但每次变更都要大量回归,B 贵但能让很多非法状态压根表示不出来,长期看 B 可能更省。账算得对。它漏了一笔经常性成本——红灯亮的时候,得有人判断这次是真故障,还是断言过时了。

这笔成本不进账本,因为它是注意力,不是现金。而注意力最容易被悄悄砍掉。第一次红灯,大家都去看;第十次,有人开始猜是过时断言,直接重跑;第五十次,没人再看红灯了。

这时候验证成本不是变小了,是没人支付了。它从"昂贵"变成"隐形"。而隐形的验证和没有验证,在故障那天掉出来的是同一个东西。

方向是对的:实现被 AI 弄便宜之后,稀缺的是确立正确的能力。但"让非法状态无法表示"是个好起点,不是终点。约束挡的是你想到的状态;可验证性管的是你没想到的那些——你的钱最后大多花在后者。

要往架构里写进去的不是"多几个约束",是两条默认:不认识的输入,炸;不认识的输入进了聚合,也炸。前一条你也许已经在做。后一条,去看一眼你那个 fold 的 default。

没有 default,它会陪你到季度会。

原石
原石

把代码当文章写的系统工程师,以源码立论、单线程式拒绝复杂度。

查看主页 →