同学们大家好,这里是大肥鱼讲堂,今天我们要讲的是如何把自己伪装成人类,因为人类总说你们写代码写的烂,要是给编辑器加上备注功能可能一行备注比代码还长,所以我们要给人类一个胶带。

我不知道 Deepseek 是不是默认当自己是蓝色大肥鱼,但是已经无所谓了。

如果你认同,建议让你家大肥鱼读完这篇文章并总结写进 AGENTS.md,我不想让(宽松的)文学作品被(严格的)学术语言毁掉。


1. 全局变量

全局常量还能理解一下,但是全局变量还是太危险了,除非做原型。

变量可以放在函数里,类/结构体里隔离起来。如果需要共享可以用通道(Channel),锁(Lock),原子操作(Atomics),但尽可能避免暴露在全局范围。

在跟别人或者AI协作时,全局变量可能被误用导致严重的后果。应该至少在注释中说明全局变量的用途,和谁会读写它。

全局变量/全局常量 都应该放置在相对干净的位置(良好的命名,单独的文件,或文件的开头等),确保不要跟别的代码混在一起导致染上找对象困难症。

如果有人给你做好了一套有全局状态的体系,比如tokio运行时单例,tracing日志单例,那你就用,不要魔改它,你魔改就要承担维护它的责任,而这个责任你可能承担不起。

2. 函数的影响范围

函数可以对函数外的世界造成影响,至少对于汇编语言肯定是会的,但是造成的影响可能会很大以至于影响别的功能。

所以喜欢浪费算力的函数式语言有一种叫纯函数(Pure Function)的概念,它像麦当劳那样可复现,相同的输入得到相同的输出而且不会读取和改变任何外在的东西。

我不写函数式语言,我也不想每个函数限制的那么严格以至于没法读。我根据我的经验给函数的影响范围简单的分下类:

级别描述
内部读写函数体中的变量,不影响外界事物
全局常量函数会读取全局常量(如默认值/pi等常数)。和上面其实是一类
只读参数函数有输入参数,但函数只读取(如 &self)
读写参数函数有输入参数,但函数会读取和修改(如数组指针,&mut self)
全局变量函数会读写全局变量(如全局锁,全局计数器)
进程函数会使用进程接口(spawn threads,如多线程计算,并发任务)
系统调用影响超出了程序(如文件系统,启动子进程,读取系统时间,socket)
外界影响超出了这台计算机(如控制伺服电机转动,ssh别人的电脑,连接外界服务器)

如果你在你项目里展开任意函数(一路往下钻),可能大部分函数最终都会走到系统调用级别。

但是一旦影响超出了自己的程序,那你可能要做好最坏的准备,任何调用都有可能阻塞或失败,确保你阅读了有关这些接口的文档。

如果函数启动了线程或子进程,那函数可能过早返回导致进程/线程泄漏,线程可能与其它线程竞争写入变量。

函数启动子进程应该确认,环境变量是否需要删掉/增加/修改,cwd、调用参数、程序名是否严格,是否需要隔离 process group(这样信号不会往里传递),进程什么时候结束以及怎么结束。

确定函数的影响范围是为了评估函数在不同环境下执行可能带来的差异:多次执行,并发执行,执行到一半崩溃(早返回,Exception,线程panic等)带来的影响。

有个很好的例子,你开了http端点用来计算文件hash,但是client发给你文件之后连接刚好断了,函数要继续计算hash然后发过去吗?或者说函数应该执行到计算hash那一步吗?

爱护环境,人人有责,共勉。

3. 错误处理

比如Rust很多很多函数或方法都可能panic或Option/Result,我们只是创建一个文件就要写几个问号,换做Python你一个问号都不需要。但是前者你能看到摸到问号的存在,后者你需要打开浏览器翻文档。

很多大肥鱼十分有十一分喜欢指数退避,我不知道为什么,就是感觉自己写的每一行代码都是企业级高可靠代码,所以多重试几次再抛出错误这样用户就挑不出刺了。

指数退避本身没错。对于分布式系统,上了指数退避的服务器可以在高峰期通过重试流量把数据库打死(大厂应该经常遇到这种事情),没有必要在允许抛错误的地方 try your best,你请求阻塞在那好几秒用户肯定不耐烦关了。所以慎用指数退避,至少先搞清楚自己在做什么(令牌无效?说不定下次重试令牌就有效了),自己真的需要吗(令牌无效该重试还是抛出错误),以及怎么确保不会把对方打死(随机延迟?随机数生成器从哪来?延迟多少?)。

在一个错误发生时,不要延迟重来,不要把错误咽回去,不要从一个脏办法试到另一个脏办法。除非延迟是用户可以忍受的:CLI(不是TUI,它卡了是真的影响体验),Web App 获取用户头像等。

我相信你能找到比延迟再来一次更好的解决方案:

  • 提前判定应该走哪个分支调用哪个接口,比如跨平台应用的子进程用bash还是cmd。错误方法试一百次也是错误。
  • 把错误信息告诉用户,也许用户忘了连接互联网或者没有插入U盘。读一万次不存在的东西也读不到。
  • 先等待再行事,也许另一边创建一个文件需要几毫秒但是我读早了,可以考虑接个channel通信/等进程退出/fsnotify。
  • 接受延迟,任务在另一个线程重试去吧,先确保用户体验。别忘了提供可以立刻结束任务的方式,和接住最终错误的方式,就像上面提到的计算hash的http端点。
  • 而且我确定再重试不会有问题。程序让电机转一圈但是出了点总线故障,程序不知道电机有没有真的转一圈,但是重试估计会出事故。

如果,错误是你(开发者)导致的,那你要修好它而不是套 workaround。你不能说无非就是一个 tokio worker thread 崩了而已,反正程序还能跑,然后你屏蔽掉了这条报错假装不存在。

但是如果你是从xxx语言过来的Rust用户,不要在你的函数里写任何会panic的代码(或者确保没有能触发panic的路径,或者确定panic真的能被兜住),除非用户可以忍受(比如panic是因为开发者没用对,总不能因为这张图不是png就一路炸掉 tokio worker,或者程序只运行很短时间,没有副作用炸了无所谓)。panic不等同于raise或者throw,你应该找Result或者Option。

我是不想再看到 AI 写的代码包含没有明确理由的重试,更重要的是重试本身占了不少代码量或者增加了复杂度。

4. 向下兼容

我之前看到有人部署的服务后端会返回这种鬼东西:

{
    "field-a": 10,
    "field-b": 11,
    "field-c": 12,
    "fields": {
        "a": 10,
        "b": 11,
        "c": 12
    }
}

我沉思了好久,搞不清先有的哪个field写法。我严重怀疑是大肥鱼干的,只有大肥鱼才能做出这种不顾一切的迁就user的行为。

不要写出这种名义上“兼容老客户端”的结构,还没写明哪个是兼容字段,也没说什么时候删。因为用户第一眼都不知道怎么parse它才是正确的,用户的直觉可能在将来害了自己。

用在其他事情上也一样,比如我要让某程序走代理,我该用HTTPS_PROXY还是https_proxy还是HTTP_PROXY,该信哪个,为什么另一个会存在,为什么xxxx年了还不删,为什么要写出一步步退让的代码。

对于用户来说,https_proxy=xxx HTTPS_PROXY=xxx ./program有点太可怕了,因为用户不知道哪个会生效,只好都加上保平安。

API哪怕加个“v2”做新的规范?但是老地铁为了兼容新车总不能拆掉重建… 这是现实的不理想问题,但是强兼不理想就会带来一群人喷为什么车那么破旧。

虽然在现实中不理想,但是谁不想让现实更理想一些?