Go 语言原本 under the hood
Go 语言原本
第 1 章 · 设计哲学与历史

1.1 编程语言的发展

要理解 Go 为何长成今天这样,为何如此克制、如此偏执于编译速度、如此看重并发,得先看它诞生时 面对的是怎样一片语言版图,以及它想替谁解决怎样的痛。Go 不是凭空设计的,它是一群写了一辈子 系统软件的人,对当时主流语言长期不满的一次回应。读懂这段背景,全书后面对调度器、内存模型、 泛型的种种取舍,才有了一个统一的出发点。

1.1.1 语言演化的几条主线

编程语言半个多世纪的演化,不是一条直线,而是几股力量的反复拉锯。把它压缩成一条时间线: 1950 年代末,Fortran 让科学计算摆脱汇编、Lisp 带来高阶函数与自动内存管理;1960 年代 Algol 确立 了结构化的块与语法;1970 年代 C 与 Unix 一同诞生,给了系统程序员一种「贴着机器又不必写汇编」的 语言;1980、90 年代 C++ 与 Java 把面向对象推向主流,前者要零成本、后者要可移植与托管;同期 Python、Perl 等脚本语言以开发效率见长;2000 年代后,Go 与 Rust 代表了「重新审视系统编程」的 两种回答,一个赌简单与并发,一个赌编译期的内存安全。

每一步背后,是在几个维度上做了一组特定取舍。没有哪门语言能在所有维度上同时最优,看清这几个 维度,才看得懂各家为何长成那样:

  • 抽象层级:从汇编的「直接操纵机器」,到 C 的过程抽象,到 C++/Java 的对象抽象,到函数式 语言的高阶抽象。层级越高,表达越凝练,离机器越远,对性能与可预测性的掌控也越弱。
  • 内存安全:从 C 的手工 malloc/free(强大但极易出错,悬垂指针、越界、泄漏),到带垃圾 回收的托管语言(消除一大类内存错误,代价是运行时开销与停顿),再到 Rust 的所有权(编译期 保证安全,代价是更陡的学习曲线)。
  • 并发模型:从手工线程加锁(危险且难推理),到 CSP / Actor 等更高层的消息传递 (1.3),到 async/await 的无栈协程。
  • 类型系统:在「静态强类型的安全」与「动态类型的灵活」之间反复摆动;又在「名义类型」与 「结构化类型」(4.2)之间各有选择。
  • 工程性:编译速度、构建模型、工具链、依赖管理。这一维度长期被语言设计者轻视,却恰是 大规模软件协作的命脉,也是 Go 着力最猛的地方。

把几门有代表性的语言摆在这张坐标上,差异一目了然:

语言内存管理并发类型编译/执行设计取向
C手工线程 + 锁静态、弱编译为本地码贴近机器,零成本抽象
C++手工 / RAII线程 + 锁静态、强、复杂编译慢、执行快表达力极大化
JavaGC线程 + 锁 / 虚拟线程静态、名义、反射丰富JIT、运行时重「一次编写,到处运行」
PythonGC(引用计数)GIL + 线程 / async动态解释执行开发效率优先
Rust所有权线程 + async静态、强、所有权编译慢、执行快安全且零成本
GoGCgoroutine(CSP)静态、结构化接口编译极快、执行快简单、工程友好

这张表不是为了排座次,没有「最好」的一行,只有「为不同目标做了不同取舍」的若干行。Go 在表里 的位置,带 GC、CSP 并发、结构化接口、编译极快、设计取向是「简单」,正是它要解决的那些痛 逼出来的。

1.1.2 设计者的血脉

一门语言的性格,往往写在它设计者的履历里。Go 始于 2007 年的 Google 内部,由 Robert Griesemer、 Rob Pike、Ken Thompson 三人发起,而这三人的背景,几乎注定了 Go 会是今天这副模样。

Ken Thompson 是 Unix 的共同缔造者、B 语言的作者(C 的前身),也是 Unix 哲学「让每个程序只做 好一件事、用简单的接口把它们组合起来」的源头之一。Rob Pike 来自贝尔实验室,是 Unix 之后的 研究型系统 Plan 9 的主要设计者,与 Thompson 一同发明了 UTF-8;更要紧的是,他在贝尔实验室二十年间 做过一脉相承的并发语言实验,Newsqueak、Alef、Limbo,把 Hoare 的 CSP(1.3)思想 逐步「语言化」,Go 的 go 关键字、channel 与 select 正是这条线的延续。Robert Griesemer 则有 深厚的编译器背景,参与过 Google 的 V8 JavaScript 引擎、Java 的 HotSpot 虚拟机与 Strongtalk。

把这三股血脉叠在一起,Go 的基因就清楚了:Unix 的极简主义(小、正交、组合)、一条二十年的 并发语言谱系(CSP 与 channel)、一线的编译器与运行时工程经验(极快的编译、厚重的运行时)。 一门由「Unix 系统程序员加并发语言研究者加编译器专家」设计的语言,天然会偏向简单、贴近系统、 且把工具链看得极重。Go 不是某个语言学新潮流的产物,而是这群人把毕生经验提炼成的一套取舍。

1.1.3 Go 诞生时的不满

促使他们动手的,是大规模 C++ / Java 服务端开发里几桩具体而日常的痛苦。Rob Pike 在 《Go at Google》中讲得很直白,Go 是「为了解决 Google 的软件工程问题」而生,而非追求语言学上的 新颖。这些痛包括:

  • 编译慢到拖垮节奏。 Google 庞大的 C++ 代码库,因头文件的传递性依赖,一个 .cc 文件 #include 一个头,那个头又 include 别的头,层层展开,一次编译可能要把同一批头文件解析成千 上万遍。Pike 举过一个被反复引用的例子:一次大型构建,编译器实际处理的头文件展开量是源码本身的 数千倍,构建动辄数十分钟。开发的「改一行、编译、看结果」循环被严重拖累。Go 后来对编译速度 近乎偏执的重视(3.215),根子 就在这里。
  • 依赖管理混乱。 #include 的文本包含模型让依赖关系不清不楚、且重复编译。Go 的包模型 (包为最小单位、禁止循环依赖、只读依赖的导出摘要而非全部源码)与「未使用的导入即编译错误」, 正是冲着这个痛去的。
  • 并发难写又危险。 C++ 的并发要靠线程库加手工锁,既啰嗦又极易出错(竞争、死锁),而 Google 的服务端程序本质上高度并发。Go 把并发做成了语言的一等公民,一个 go 关键字、 channel 与 select1.39),让并发从「专家才 敢碰」变成「人人能写」。
  • 语言过于庞杂。 C++ 特性繁多、相互交织,以至于一个团队里,对「该用语言的哪个子集」都难有 共识,新人要花很久才敢碰某些角落。Pike 多次提到,正是某次等待一个大型 C++ 程序编译、顺手翻 C++0x 标准草案的经历,让他下定决心做一门「小」语言。

1.1.4 一种「少即是多」的回应

Go 的回应,是一种刻意的克制。它选择了:编译为本地机器码、带垃圾回收、静态类型;一套极简的 语法(仅 25 个关键字,少到能一口气背完);以 CSP(1.3)为蓝本的并发;以组合而非 继承为骨架的类型系统(4.2);以及对工具链与构建速度的 极度重视。

有意不要的东西,和它要的东西同样醒目,且每一项「不要」都对应着上面某个痛或某条价值:

  • 很长时间没有泛型(直到 2022 的 Go 1.18,8),因为没找到 一个不牺牲编译速度与简单性的方案,宁可不做也不草率。
  • 没有异常7),错误是普通的值,显式返回、显式处理,拒绝 「隐式的控制流跳转」。
  • 没有继承,用接口与组合替代,避免类层级的脆弱与纠缠。
  • 没有运算符重载、没有隐式类型转换,拒绝「看一行代码猜不出它在干什么」的魔法。

Pike 把这种取向凝成一句话,「少,但指数级地多」(Less is exponentially more):删掉一个特性, 省下的不只是那个特性本身,还有它与其余每个特性两两交互所滋生的复杂度,后者随特性数量呈组合爆炸。 一门小语言之所以可贵,正因为它的特性之间没有那么多暗处的纠缠。

1.1.5 复杂度必须挣得它的位置

「用减法做设计」的取向,是贯穿全书的主线。一个浓缩的例子是 Russ Cox 2009 年提出的 「泛型的两难」(the generic dilemma):在「慢的程序员、慢的编译器、慢的运行时」之间,任何 泛型方案似乎只能三选其二,C++ 模板选了快运行时(牺牲编译速度与代码体积),Java 选了快编译 (牺牲运行时的装箱开销)。正因为 Go 极度看重后两者(编译快、运行时简洁),它宁可让程序员 暂时多写点重复代码,也迟迟不引入会牺牲它们的泛型,直到 2022 年找到一个能较好平衡三者的实现 (GC 形状 stenciling 加字典,8.1)才出手。这十三年的 等待,是 Go 设计哲学最鲜明的注脚:复杂度必须挣得它的位置,新特性要先证明自己值得。

理解了 Go 诞生时的这些不满与回应,本书后面对每一处实现的剖析,为什么调度器是协作加信号抢占 (9.7)、为什么内存模型只给顺序一致原子 (11.9)、为什么 GC 死磕低延迟 (13),就都有了一个共同的坐标原点:它们都是「简单、工程友好、为 真实的大规模软件协作服务」这组价值,在各个角落的具体展开。下一节(1.2)先给全书 搭一张鸟瞰图,把这组价值如何分布在语言、运行时、工具链三层里看个明白。

延伸阅读的文献

  1. Rob Pike. Go at Google: Language Design in the Service of Software Engineering. 2012. https://go.dev/talks/2012/splash.article (Go 的工程动机,最权威的一手论述)
  2. Russ Cox. The Generic Dilemma. 2009. https://research.swtch.com/generic
  3. Rob Pike. Less is exponentially more. 2012. https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html
  4. The Go Authors. Frequently Asked Questions (FAQ): Origins / Design. https://go.dev/doc/faq
  5. Rob Pike. The Implementation of Newsqueak / Concurrency in Go 的语言谱系. (Newsqueak/Alef/Limbo 到 Go 的 CSP 一脉)
  6. Brian W. Kernighan. Unix: A History and a Memoir. 2019 (Go 设计者的 Unix / Plan 9 渊源).
  7. Alan A. A. Donovan, Brian W. Kernighan. The Go Programming Language. 2015.