没有谷歌规模就别用protobuf祸害自己人


前谷歌工程师血泪控诉 protobuf 如何以“兼容”之名行“摆烂”之实,类型系统支离破碎,语义陷阱防不胜防,最终污染整个代码库。你不是谷歌,别为省几个字节赔上工程师的命。

作者曾经在谷歌工作,亲身经历过 protobuf 的“洗礼”,也亲眼见证了这套序列化工具如何从一个内部工程妥协方案,演变成横跨整个代码库的“技术债务瘟疫”。

他不是门外汉喷产品,而是从一线实战中踩过无数坑后,带着血泪经验写下这篇檄文,目的不是为了黑谷歌,而是想唤醒那些盲目跟风“谷歌用啥我用啥”的技术团队——你不是谷歌,别把自己当谷歌。

先说最扎心的一点:protobuf 的类型系统简直像实习生边喝奶茶边搭出来的乐高积木。

它表面上支持各种数据结构,但每一个功能之间都互不买账,彼此打架。
比如:你不能把 oneof 字段设成重复、
map 的键不能是字节或枚举、
map 的值不能是另一个 map、
用户自定义类型完全不能泛型化。

这些限制不是因为技术上做不到,而是因为当初设计时根本没考虑类型系统的统一性和扩展性,全是事后打补丁凑出来的。

这种设计风格,说好听点叫“实用主义”,说难听点就是“业余选手的即兴发挥”。

更离谱的是,谷歌把字段分为“标量类型”和“消息类型”,然后给它们安排了完全不同的语义。
标量字段永远有默认值,哪怕你压根没传,系统也会给你塞个零或空字符串,而且你根本分不清这是用户传的还是系统补的。
消息类型更魔幻,访问一个未设置的字段,系统会当场给你 new 一个默认实例,但如果你对这个实例做修改,居然会反过来污染原始消息体。

这种“读时无害,写时爆炸”的设计,直接违反了编程世界最基本的“赋值不变性”原则——你写 msg.foo = msg.foo,居然能凭空给 msg 增加一个字段,数据凭空蒸发的风险就在这一行代码里埋下了。

再说那个被吹上天的“向后向前兼容”:听起来很美好,其实本质是“摆烂式兼容”——系统对任何畸形数据都来者不拒,给你塞默认值,让你程序跑起来不崩溃,但跑出来的结果是不是你想要的?没人管。
开发者被迫在每一个使用点写防御性校验,本来应该在反序列化入口统一处理的逻辑,被分散到整个代码库的犄角旮旯。
这哪是兼容?这是把技术债分批发给每个程序员,让大家轮流还利息。

更讽刺的是,谷歌官方风格指南居然鼓励你“不要抽象、不要复用、尽量内联”,理由是“万一以后字段语义变了,分开定义好改”。这相当于为了应对未来可能发生的极小概率事件,主动放弃六十年软件工程积累的最佳实践。
这种思维只有在谷歌那种工程师工资占比极低、网络带宽和存储成本才是大头的超大规模场景下才成立。
而你,一个中小团队,一个创业公司,工程师是你最贵的资产,你居然为了省几个字节去浪费他们的时间?这不是优化,这是自残。

最致命的是,protobuf 会像病毒一样感染你的整个代码库:
因为它生成的类太死板、太僵硬,根本没法和你业务里那些灵活多变的数据结构共存。

你只有三个选择:
要么自己再写一套平行类型,同步维护两套结构,累死;
要么直接拿 protobuf 当业务对象用,把网络层的限制带进业务逻辑,蠢死;
要么每次用的时候现场转换,性能炸裂,烦死。

作者亲身经历,在谷歌内部做过一个编译器项目,输入输出都是 protobuf,结果因为类型系统太弱,原本五十行能搞定的递归算法,硬生生写成上万行的手动内存搬运代码,完全没法用现代编程范式重构,简直是精神折磨。

说到底,protobuf 是谷歌在特定历史阶段、特定成本结构、特定工程文化下诞生的产物。它解决的是谷歌自己的问题,不是你的问题。你没有谷歌的规模,没有谷歌的成本模型,没有谷歌的工程冗余,却非要硬搬谷歌的工具,结果就是削足适履,自讨苦吃。

那些吹“protobuf 是工业标准”的人,要么没真用过,要么在谷歌领工资,要么纯粹跟风。

真正的好工具应该让代码更清晰、更安全、更易维护,而不是让你每天提心吊胆怕字段被偷偷重置、怕默认值掩盖真实意图、怕类型系统限制创造力。

别再把“谷歌用”当成技术选型的免死金牌了!技术选型要看场景、看成本、看团队、看未来。你不是谷歌,你的问题谷歌没兴趣帮你解决,你的工程师时间比带宽贵一万倍。

醒醒吧,扔掉那些让你半夜惊醒的 oneof 嵌套,告别那些让你怀疑人生的默认值陷阱,选一个真正尊重类型系统、尊重开发者心智、尊重软件工程原则的序列化方案。你的代码会感谢你,你的团队会感谢你,你的老板——如果他还想活着见到下一轮融资——也会感谢你。