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 线程 + 锁 静态、强、复杂 编译慢、执行快 表达力极大化
Java GC 线程 + 锁 / 虚拟线程 静态、名义、反射丰富 JIT、运行时重 「一次编写,到处运行」
Python GC(引用计数) GIL + 线程 / async 动态 解释执行 开发效率优先
Rust 所有权 线程 + async 静态、强、所有权 编译慢、执行快 安全且零成本
Go GC goroutine(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.2、15),根子 就在这里。
  • 依赖管理混乱。 #include 的文本包含模型让依赖关系不清不楚、且重复编译。Go 的包模型 (包为最小单位、禁止循环依赖、只读依赖的导出摘要而非全部源码)与「未使用的导入即编译错误」, 正是冲着这个痛去的。
  • 并发难写又危险。 C++ 的并发要靠线程库加手工锁,既啰嗦又极易出错(竞争、死锁),而 Google 的服务端程序本质上高度并发。Go 把并发做成了语言的一等公民,一个 go 关键字、 channel 与 select(1.3、9),让并发从「专家才 敢碰」变成「人人能写」。
  • 语言过于庞杂。 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.