《Google软件工程》教会我的

内容管家 编程开发评论3字数 5940阅读19分48秒阅读模式

《Google软件工程》教会我的

《Google 软件工程》教会我的那些事

一本被书名耽误的宝书

最初拿到《Software Engineering at Google》时,我以为又是一本 Big Tech 的炫富之作——里面的经验只有坐拥数十亿用户、数万名工程师的公司才用得上。但读进去才发现,这本书讲的东西放之四海而皆准,无论团队只有 5 人还是 5000 人,都能受益。

这本书脱胎于 Google 20 多年的工程实践:代码规模超过 20 亿行,每周变更 2500 万行。核心不是教你如何写代码,而是 Google 如何在漫长岁月里保持代码库健康——即代码写完之后的事情:如何演进、如何共享、如何测试、如何最终删除。

软件工程 ≠ 编程

这是全书最核心的观点,由 Titus Winters 在第一章中阐述,读完彻底改变了我对工作的认知。

我们日常把"编程"和"工程"混着用,但两者根本不是一回事:

  • 编程:拿到一个任务,写代码解决它,测试变绿,交付,走人。
  • 工程:代码写完之后,才是真正的开始。你要问的是——
  • 为什么要做这个?
  • 对用户有什么影响?
  • 需求变化时,代码如何演进?
  • 不只是技术层面,如何在组织层面扩展?

Titus 的原话是:"软件工程是跨越时间维度的编程。"这句话的分量极重:每一行代码都有寿命,工程师的职责是思考这个寿命的全部成本,而不只是写代码那段愉快时光。

一个黑五营销页的快速脚本?那是编程。十年内要处理数百万笔交易的支付系统?那必须靠工程。

Hyrum 定律与碧昂丝法则

这是本书我最喜欢的两个概念,它们放在一起讲是有原因的。

Hyrum 定律

这可能是全书被引用最多的概念,以书中主编之一 Hyrum Wright 命名:

任何 API,只要用户足够多,无论你在契约里怎么承诺,系统所有可观测行为都会被某些人依赖。

听着很理论,直到你被它坑过。经典案例是哈希迭代顺序。Java HashMap 文档明确写明"不保证任何顺序",但 Google 升级 Java 版本时(哈希顺序会变化),大量测试直接失败——因为工程师们写了 containsElementInOrder() 这种依赖顺序的断言。更离谱的是,有人还用哈希迭代顺序当低效的随机数生成器。Google 的解法是"防御性随机化":直接修改 JDK,让哈希迭代顺序每次运行都不同,让依赖未规定行为变成不可能。Python 和 Go 也独立做了同样的事。所以关键教训是:指责工程师没用,要让错误根本不可能发生

Hyrum 定律在 Google 之外同样适用。2024 年,Recall.ai 发布了一个变更,在 S3 URL 路径前加了一个冒号(URL 中完全合法的字符)。结果客户的应用全挂了——因为一个三层传递的依赖(yarl 库)会自动标准化 URL,将"安全"字符做 URL 解码,破坏了 S3 签名。没有人直接解析过那些 URL,但三层深处的一个依赖已经建立了隐性契约。

碧昂丝法则

这条法则的名字来自她的歌词,意思是:如果你依赖某个行为,就应该为它写测试。 举个例子:你修复了一个小 bug,看起来不是什么大事。但账单组的同事 Joe 恰好依赖这个"bug"让他的代码正常工作。你的修复一出,Joe 的代码就崩了。怪谁?

碧昂丝法则的答案是:如果 Joe 看重那个行为,他早该为它写测试。当你的修复导致 Joe 的测试失败,你会立刻看到反馈:"哦,还得顺便修一下 Joe 的代码。"由于 Joe 测试了他在乎的一切,你不需要完全理解他那堆复杂代码的细节,就能把它修好。

Google 内部的做法是:收到 bug 报告并准备修复时,工程师首先写测试来证明 bug 已被修复。测试方法的注释区会标注 Jira 工单号,便于追溯到原始 bug。

这条法则的深层含义是共享所有权。在大型代码库中,任何人都可能修改你的代码。测试成为一种沟通机制,向整个团队说明"哪些行为对我来说是重要的"。没有测试,就只能靠口口相传,而那无法规模化。

三、左移测试

这个概念简洁而有力:越早发现问题,修复成本越低。 2005 年,Google 搜索的基础设施 Google Web Server(GWS)处于危机状态——超过 80% 的生产环境发布都会引发影响用户的 bug,不得不回滚。技术负责人因此强制要求工程师自动化测试新代码。一年内,尽管新变更数量创下纪录,bug 却减少了一半。如今 GWS 拥有数万测试用例,每天发布,却很少出现面向用户的故障。

这完美解释了 Google 的测试哲学。书中推荐一个相当陡峭的测试金字塔:

  • 约 80% 小型测试(单元测试)
  • 约 15% 中型测试(集成测试)
  • 约 5% 大型测试(端到端测试)

关键区别在于:Google 按测试规模(资源消耗)而非传统的单元/集成分类来划分测试。小型测试在单进程、单线程中运行,不涉及任何 I/O、网络或磁盘访问;中型测试可使用本地主机的多个进程;大型测试则可跨多台机器运行。核心优化指标是速度和确定性。

关于不稳定测试(flaky tests)的描述也值得注意。Google 的不稳定率约为 0.15%,但对于那样规模的单体仓库,每天仍会产生数千个不稳定测试。书中指出,如果不稳定率接近 1%,测试就会彻底失去意义——工程师会直接无视它们。

Google 通过三项文化举措将测试理念推广到全公司:

  • "测试认证"(Test Certified)——一个五级别的半年期项目,通过公开排行榜利用社会压力,推动 1500+ 项目采用测试流程。
  • "厕所测试"(Testing on the Toilet)——2006 年 4 月起在卫生间张贴的单页测试建议。作者形容这是"持续时间最长、影响最深远的测试推广举措"。

从实操角度看,左移意味着在开发流程中加入大量质量关卡:

  • 静态分析在编辑器中即时运行,发现拼写错误、函数调用错误和类型不匹配。修复成本:秒级。
  • 单元测试几秒完成,验证代码按你的预期工作。修复成本:分钟级。
  • 集成测试耗时数分钟,验证系统各组件协同是否正常,能捕获单元测试遗漏的边界情况。修复成本:一小时左右。
  • 代码评审耗时数小时,是人工质量把关,回答"代码是否符合团队规范、方案是否合理"等问题。这也是团队知识共享的最佳机制之一。修复成本:可能半天。
  • QA 耗时数小时到数天,验证一切是否如预期协同工作。修复成本:数天到一周。
  • 生产环境用户会发现你遗漏的一切,以及你从未想到过的边界情况。修复成本:可能巨大,技术上和名誉上都是。

越靠右,修复代价越高。这就是 Google 大力投入 Tricorder 静态分析工具、快速单元测试基础设施,以及提交前检查的原因——在人工介入之前,尽可能多地发现问题。

四、慎用 Mock 框架

第十三章关于测试替身的内容可能是全书最令人意外的建议:尽量使用真实实现而非假对象(fakes)和桩(stubs),把 Mock 作为最后手段。 Mock 框架刚进入 Google 时,大家觉得"像是锤子看什么都像钉子",写针对性测试非常容易。但代价很快显现:测试变成了"需要持续维护,却很少真正发现 bug"的东西。如今风向已经大幅转向,许多 Google 工程师干脆不再使用任何 Mock 框架。

单元测试:Moco 的局限与 Fakes 的优势

Mock 的根本问题在于,它验证的是"某段代码以何种方式执行",而非"执行后产生了什么结果"。以支付处理器的测试为例,Mock 测试能检查 pay() 方法是否以正确参数被调用,却无法确认支付是否真正完成。Fakes(替身实现) 则不同——它保留内部状态,开发者可以调用 processPayment() 后,通过 getMostRecentCharge() 查看实际发生了什么。

Google 甚至在 ErrorProne 静态分析工具中内置了 @DoNotMock 注解。当某个 API 在代码库中被 Mock 了上万次,任何重构都可能破坏这些测试——即便支付功能本身运行正常。

关键取舍在于维护成本。 Fakes 需要投入专门资源建设,通常由真实实现的团队维护,并通过契约测试(Contract Tests)确保两者行为一致。如果消费者数量有限,建设 Fakes 或许不值得;但对于高频使用的核心 API,其对研发效率的提升是巨大的。

代码审核&查验的核心价值:不是查 Bug,而是传递知识

大多数团队将代码审核&查验视为质量关卡——有人检查逻辑,点击批准,变更上线。Google 的做法截然不同,而这一差异的影响远超预期。

Google 明确指出:代码审核&查验的首要收益不是正确性校验,而是理解增强、知识共享和长期可维护性。一段没人能理解的代码,即便运行无误,也已经是问题。无法理解的代码无法修改,无法修改的代码无法维护。

Google 的审核&查验流程比多数团队想象的更体系化,包含三个层次:

  • Peer LGTM:这段代码正确且可理解吗?
  • Owner 审批:这段代码适合这部分代码库吗?
  • 可读性认证:代码是否符合语言规范?

三个角色可由同一人承担(通常也是如此),但拆分它们至关重要——每个角色审视不同维度,避免单一审核&查验者同时面对全部压力。

两个机制支撑整个系统运转: 变更保持在 200 行以内,反馈在 24 个工作小时内完成。Google 三分之一的变更仅涉及单个文件。小变更被仔细阅读,大变更则被略读——这是人性,Google 选择顺应而非对抗。

写代码的人:放下自我,接受审核&查验

"You are not your code." 代码提交审核&查验的那一刻,它就不再属于你。每条审核&查验意见都是一项待办,而非对你个人的否定。即便不同意某条意见,也应解释原因并请对方重新审视代码。

五个值得上蔷的实践

  • 保持礼貌与专业:发现问题时先询问,而非假定对方出错。你可能缺少上下文。作为作者,将每条意见视为 TODO,而非终审判决。
  • 提交小规模变更:200 行的 Diff 会被人阅读,2000 行的 Diff 只会被批准。聚焦单一问题,反馈才会真正有用。
  • 写好变更描述:描述第一行会出现在邮件主题、代码搜索和未来调试中。"修复 Bug" 毫无信息量,应一句话说明变更内容和原因。
  • 审核&查验者尽量精简:一人通常足够,更多审核&查验者意味着更慢的周转和分散的责任。需要第二意见时,明确提出。
  • 自动化能做的事:静态分析、格式化、测试覆盖率——机器能捕获的问题不应占用人工审核&查验的注意力,留给人判断真正需要决策的事项。

小步快跑:高频发布的重要性

小规模发布更容易管理、理解和回滚。这个道理说起来简单,但令人惊讶的是,仍有大量团队将数周工作打包成一次大规模部署,然后用接下来三天时间排查问题。

逻辑很清晰:生产环境出现问题时,小规模发布能立即定位是哪个变更引发的。包含 50 个提交的大版本?祝你好运。

Google 在公司范围内以"每秒提交数"追踪开发效率。为此,他们投入建设 CI/CD 基础设施,使小变更部署变得轻松。功能开关(Feature Flags) 实现了"部署代码"与"开启功能"的解耦,允许代码持续部署,即便功能尚未准备好面向用户。

DORA(DevOps 研究与评估) 的研究用数据证实了这一点:实践持续交付、每日或每小时发布节奏的团队,在速度、质量、稳定性和开发者满意度各方面均表现更优。

Abseil book 的作者 Titus Winters 明确引用 DORA 研究 作为证据:trunk-based development(主干开发)和持续交付带来的技术收益是因果关系,而非仅仅相关。

依赖升级:早升级、快升级、频繁升级

与小步快跑的发布策略同理,但针对第三方代码。

从 4.5.8 升级到 4.5.9 没什么大不了——补丁版本更新,快速测试即可完成。从 4.5.8 升级到 4.8.0 可能需要一些调整,涉及部分废弃 API 和配置变更,但一下午也能搞定。

从 4.5.8 跳级到 7.0.0?光是想想就让人头疼。哪怕所有中间版本都做到了向后兼容,跨度太大本身就是风险。隐式接口早已面目全非——这正是Hyrum 定律在作祟——升级本身就能变成一个独立项目。

书中对此毫不讳言:"软件工程最难解决的未解问题,是依赖管理。" Google 的单体代码库系统在这方面优势明显,但对大多数团队来说,最佳解法朴实无华:保持依赖始终最新。小步快跑、频繁迭代,永远比攒个大的再迁移省钱省心。

还有一个被低估的洞见:更新工作应由专家来完成,而非依赖方。当你废弃某个函数时,别只发通知说"请自行升级"——没人会真的去做。你得亲自上手改。这道理很简单:你是这个领域的专家,最清楚需要改什么,速度也最快。反观使用者,他们得切换上下文、阅读迁移指南、在迭代计划里挤时间……结果可想而知。

八、生产力必须可测量

第 11 章值得单独成节,因为它引入了一套叫目标 / 信号 / 指标(GSM)的框架——堪称全书最具实操价值的工具之一。

在动手测量之前,Google 团队会先问一组筛选问题:这件事值得测量吗?结果会改变决策吗?决策者信任我们将产出的数据类型吗?三个问题任一回答"否",就直接放弃,别白费力气。在这个人人沉迷指标的行业中,这本身就算得上激进了。

GSM 框架三层次

确认值得测量后,Google 将其结构化为三个层级:

  • 目标(Goal):期望的最终结果,表述中不出现任何具体指标。(例如:"工程师能写出更高质量的代码。")
  • 信号(Signal):判断目标是否达成的依据,是你想测量却可能无法直接测量的东西。(例如:"工程师反馈从过程中有所学习。")
  • 指标(Metric):信号的可行替代,是实际拿来测量的代理变量。(例如:"通过问卷调查,报告学到四个相关主题的工程师比例。")

GSM 的核心价值在于规避"路灯效应"——避免去量容易测的,而不是重要的。从目标自上而下推导,确保每个指标都能追溯到真正有意义的东西。

QUANTS 五维模型

Google 团队将生产力分解为五个维度,防止提升某一方面时悄悄损害另一方面:

  • 代码质量:产出的代码质量如何?
  • 注意力:工程师能否进入心流状态?是否频繁被打断?
  • 智力复杂度:任务需要多少认知负荷?
  • 节奏与速度:工程师完成工作的效率如何?
  • 满意度:工程师对工具、产品和工作的满意程度如何?

书中有一段直白警告:"如果生产力指标被用于绩效评估,工程师会迅速学会作弊,这些指标就失去意义了。" 唯一可行的做法是测量整体效应,绝不针对个人。Google 的生产力研究团队专门配备了行为经济学家,就是为了理解激励结构、防止古德哈特定律侵蚀数据。

一个出人意料的结论:"在 Google 的经验中,当定量指标与定性指标出现分歧时,原因往往是定量指标没有真正捕捉到预期结果。" 关键结论:如果一项测量无法转化为行动,就不值得做。 每项研究结束后,Google 团队都会准备一份具体建议清单——新工具功能、文档改进、流程调整。如果结果驱动不了任何行动,这次测量就是浪费。

九、文化篇:没人愿意聊的东西

书的开篇章节探讨了文化议题:团队协作、知识共享、心理安全与领导力。正如 Titus Winters 在 GOTO 演讲中所说,软件工程归根结底就两件事:时间与人。我们教会工程师写代码,但一旦加入团队,就成了团队运动。

几个让我印象深刻的观点:

  • "因为我是老板"是一种领导力失败。有分歧时,解释你的推理过程,通过教学而非权威引导人们改变决策。知识型工作者更愿意跟随服务型领导者,而非权威型。
  • 主动问"傻问题"。Titus 主持 C++ 标准小组委员会时,故意提出幼稚问题,确保每位成员在投票前真正重新学过这些材料。示范"不知道没关系",才能营造出让人真正学习的文化。
  • 把任务委托给能胜任的最 junior 的人(配以适当监督)。这直接反驳了 Fred Brooks 的"外科手术团队"模式。团队由此成长,高级工程师得以腾出手去解决下一个不可能的问题。
  • 大多数团队失败的原因不是代码烂,而是缺乏信任。追溯任何团队冲突的根源,你会发现问题总出在三件事上:谦逊(认为自己的方式是不可替代的唯一答案)、尊重(不再关心这个人而只关心工作)、或信任(宁可亲自动手也不愿让别人主导)。
  • 通过文档从错误中学习。坏事发生时——比如线上故障——写一份事后分析报告。报告不是为了追责,而是趁记忆还热乎时记录真相。内容包括完整时间线、真正根本原因(不是"人为失误"这种泛泛之词),以及带有明确责任人和截止日期的行动项。大多数团队在"经验教训"这一节敷衍了事,这是问题所在——这一节才是让下一次故障更快结束的关键

作者自己也不看好的做法

Titus Winters 在 GOTO 播客访谈 中坦承,书中依赖管理章节给他带来了"字面意义上的噩梦"——虽然积累了大量"什么行不通"的想法,但始终没有找到一套低成本、可扩展的解决方案。他的原则是:"我宁可面对一百个版本控制问题,也不愿面对一个依赖管理问题。"

语义版本的局限性

书中对语义版本(SemVer)提出了尖锐批评:SemVer 本质上是一种"估算",而非兼容性的"证明"。举例来说,函数 Foo 变更导致 Bar 函数 breaking change,触发主版本号升级,但实际只使用 Foo 的消费者也会被拦在升级门外——尽管他们的代码根本不受影响。更讽刺的是,理论上"安全"的补丁版本号更新,同样会频繁违背 Hyrum 定律。

文化章节的"理想化"成分

Winters 承认,文化相关章节写得"有点理想化",并不能完美描述 Google 的真实状态。外界也指出了一个明显的矛盾:Google 一边建议"不要发布没有长期支持计划的东西",一边却维护着那个臭名昭著的 Google 产品墓地

缺乏实证支撑

这本书的一个"真正弱点"是:除了"这在 Google 有效"之外,几乎不提供 Claims 的实证依据。如果能补上更多数据支撑,这本书的价值会更高。

明天就能落地的建议

作者明确提醒:不要盲目复制 Google 的做法,要理解方法背后的"为什么",再根据自身场景调整"怎么做"。以下建议具有普适性:

  • 测试一切你关心的东西:碧昂丝原则在任何规模下都有效
  • 审核&查验流程,不只是代码:明确谁负责正确性、所有权和可读性的签字;若一人包办所有环节,就是瓶颈
  • 投资构建系统和 CI 流水线:长期回报率极高
  • 小步快跑、频繁发布:把当前发布周期减半试试
  • 保持依赖最新:配置好 Dependabot 或 Renovate,别再忽略那些 PR
  • 用 Fake 替代 Mock:测试套件会感谢你
  • 有目的性地度量:创建任何新仪表盘或指标前,先用 GSM 框架
  • 像对待代码一样对待文档:审核&查验、维护,过时了就废弃
  • 营造心理安全感:这是一切的基础

Goodreads 软件工程书榜的启示

作者分析了 Goodreads 上排名前 100 的软件工程书籍,得出几个有趣的结论:

  • 高评分不等于高阅读量:《The Phoenix Project》有 49000+ 评分(4.26),《Clean Code》23000+(4.36),但《Designing Data-Intensive Applications》以 4.7 分击败两者,而评分数仅 10000
  • 经典老书依然能打:1978 年的 K&R《The C Programming Language》评分 4.44;1984 年的《Structure and Interpretation of Computer Programs》评分 4.47——经典不是情怀,是标准
  • 系统设计内容崛起迅速:Alex Xu 的《System Design Interview》系列双双进入前 15,而五年前这些书根本不存在
  • 榜尾同样有信息量:《Effective DevOps》只有 3.41,一些架构模式书在 3.7 附近徘徊——高期待碰上中等执行

完整榜单可在 GitHub Gist 查看。

延伸阅读

 
内容管家

发表评论