有一次线上事故,排查到最后,原因并不复杂。
不是架构问题,不是算法问题,甚至和业务逻辑也没太大关系。 问题出在一行看起来再普通不过的日志上。
代码能编译,通过测试,平时也跑得好好的。 直到某天,一个不太常见的分支被触发,程序在打印这行日志时直接崩了。
很多同学看到这里,第一反应是:
“哎,又是手滑。”
但我后来意识到,这个事故真正暴露的,并不是“人不够细心”, 而是我们对日志这件事,长期抱着一种过于乐观的假设。
一、打印日志,只是个小问题吗?
在大多数项目里,日志往往处在一个很尴尬的位置。
谈到架构,是要认真讨论的;
聊到性能,必须得反复压榨;
涉及到算法,要推导复杂度;
而框架、中间件,要选型、要对比。
而日志呢?—— 往往是“能打出来就行了,要求那么多干啥。”
但现实恰恰相反。 日志往往是:
- 调试的入口;
- 排查问题的最后防线;
- 也是程序崩溃前,留下来的最后一句话;
如果连“这句话”本身都不可靠,那问题就不只是“打印方式好不好看”了。
二、printf:快,但很危险
先说最常见的方式:printf。
printf 非常成功,成功到我们已经习惯了它的风险。
它最大的问题不在于“难用”,而在于:它把错误,系统性地延后到了运行期。
格式字符串是一套规则,参数列表则又是一套规则。 两者之间的关系,靠的是程序员的记忆力。
编译器在一旁看着,帮不上什么忙(实际上有 -Wformat 等编译选项)。
于是就会出现一种非常经典的工程场景:程序跑得好好的,业务处理得也很顺,某个分支里打了一行日志,然后服务就没了。
你坐着火车,吃着火锅,还唱着歌, 突然就被一条日志给“劫”了。
在工程代码里,我们当然很少直接调用 printf。 更多时候,是 snprintf、asprintf 等,或者再包一层日志宏。
但这并没有改变一个事实:它们都是使用同一套“基于格式字符串”的机制。
三、流式操作:安全,但不太像人写的
那不用 printf,改用 iostream 流的方式行不行?
当然行,而且从类型安全的角度看,它是正确的。
但问题在于: 它解决的是机器的正确性,牺牲的是人的表达效率。
当你只是想快速、清楚地描述一段状态时,却要写出一长串 <<, 大脑的注意力,很容易从“我想说什么”,转移到“我怎么拼出来,而且要拼的更好看”。
于是你会发现,两条路都不太让人满意:
- 一条路写起来顺手,但暗藏风险;
- 一条路足够安全,但表达起来很别扭;
那就真的没得选了吗?
四、问题不是 C++ 做不到,而是时代变了
后来我经常写一些 Python 脚本。
比如自动化测试,接口校验,临时工具。 在 Python 里,打印日志是一件非常自然的事。
你不用告诉解释器变量是什么类型, 也不用维护一份额外的“格式说明”。
这并不是 Python 更高级, 而是它默认了一个非常重要的前提:
类型,不应该靠程序员来记住。
这一点,一旦意识到,就很难再回去了。
有的朋友可能要杠了:那 Python 也有类型啊,甚至还有类型注解。
这个反驳,本身就说明他没意识到:
“有类型”和“靠人记类型”,是两件完全不同的事。
在 Python 里,你可以完全不写类型,程序依然是类型安全的。
其实,Python 的类型,是“帮助你忘记类型”的。
不扯了,跑题了已经。。。
五、C++ 终于开始补这一课
再后来,我发现 C++20 标准里多了 std::format。
这并不是一次语法层面的“锦上添花”, 而是 C++ 在承认一件事:
日志、字符串格式化,也应该进入类型系统的保护范围。
std::format 解决的不是「写法好不好看」,而是:
- 类型安全;
- 表达上的一致性;
- 让更多错误更早暴露在编译期;
当然,现实世界没有那么理想。
很多项目还停在 C++03、C++11。
我们自己的项目,也是因为依赖库的要求,才整体迁移到 C++17。
于是 fmt 这样的库,成了一个很现实的选择:它不是炫技,而是可以让你把未来的标准,提前带进今天的工程里。
六、最后
日志这件事,本质上并不复杂。
真正复杂的,从来不是 %d、%s、%lu、<< 这些符号,而是你需要在脑子里同时维护两套信息:一套是业务含义,一套是变量类型。
printf 和 iostream 是巨人,它们解决了属于那个时代的问题。
但今天的软件复杂度已经完全不同了,
系统的稳定性,越来越依赖于:错误能不能尽早暴露。
当一行日志需要你记住变量类型时,事故其实已经埋下了。
真正的问题不是这些工具不够强大,而是——
类型,不应该再由程序员来维护。