《Go 语言原本》

1.1 编程语言的发展

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

1.1.1 语言演化的几条主线

编程语言半个多世纪的演化,可以粗略看作几股力量的拉锯。沿着几个维度去看,每一门有生命力的 语言,都是在这些维度上做了一组特定取舍的产物,没有哪门语言能在所有维度上同时最优。

  • 抽象层级:从汇编的「直接操纵机器」,到 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 诞生时的不满

Go 始于 2007 年的 Google 内部,由 Robert Griesemer、Rob Pike、Ken Thompson 三人发起。这几位的 履历本身就是 Go 的基因:Thompson 是 Unix 与 C 的共同缔造者,Pike 来自贝尔实验室、是 Plan 9 与 UTF-8 的设计者之一,Griesemer 则有 V8 与 HotSpot 的编译器背景。一门由「Unix 一脉的系统程序员 加编译器专家」设计的语言,天然会偏向简单、贴近系统、且极重工具链。

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

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

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

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

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

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

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

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

延伸阅读的文献

  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. The Go Authors. Frequently Asked Questions (FAQ): Origins / Design. https://go.dev/doc/faq
  4. Rob Pike. Less is exponentially more. 2012. https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html
  5. Brian W. Kernighan. Unix: A History and a Memoir. 2019 (Go 设计者的 Unix / Plan 9 渊源).
  6. Alan A. A. Donovan, Brian W. Kernighan. The Go Programming Language. 2015.

许可

© 2018-2026 The golang.design Initiative Authors. Licensed under CC-BY-NC-ND 4.0.