卷 I · 减法CH 01深度 01/24

你会 Kotlin,所以先学会「卸载」

本章卸下「语言越多特性越强」这个信念

你学 Go 会卡壳,几乎从来不是因为某个概念太难,而是因为你的手比脑子快 —— 你的手指头想去按 Kotlin 的那个键。想写 try,Go 说没有;想写 class Dog : Animal,Go 说没有;想 user?.name,Go 说没有。这一章不教你任何 Go 语法,它只做一件事:让你明白这些「没有」不是缺陷,是 Go 用力删出来的结果 —— 然后你才学得进去。

设计哲学减法可读性gofmt

一个让你不适应的实验

先看两段都在打印 "hello" 的代码。左边是你熟的,右边是 Go:

// Kotlin:一行搞定,还能更短
fun main() = println("hello")
package main

import "fmt"

func main() {
    fmt.Println("hello")
}

你的第一反应大概是:Go 怎么这么啰嗦?要写 package,要 import,要 fmt. 前缀,一个 hello 五行。Kotlin 一行。如果你只看这个例子,Go 完败。

但请把这个「啰嗦」的判断先挂起来。因为你正在用一个错误的尺子量它 —— 你在量「写这一行有多快」。而 Go 从第一天起,量的就不是这个。它量的是:三年后,一个从没见过这段代码的人,多快能看懂它、并且不看错。

在那把尺子下,Go 的每个「啰嗦」都变成了优点:fmt.Println 让你一眼知道这个函数来自 fmt 包,不用满世界找它定义在哪;显式的 import 让你知道这个文件到底依赖了什么;没有 main = println(...) 那种简写,意味着整个语言里 main 函数永远长一个样

◆ Go 的那把尺子

「代码被阅读的次数,远远多于被编写的次数。」这是 Go 团队反复强调的第一原则。你写一行代码只写一次,但它会被 review、被调试、被半年后的你、被接手的同事读几十上百次。

Go 的所有别扭之处,几乎都能用一句话解释:它宁可让「写代码的这一个人」慢一点、啰嗦一点,来换「读代码的很多人」快一点、少看错一点。你觉得它在为难你,其实它在替未来那些读者,向现在的你收税。

Go 是从哪种痛里长出来的

要理解一门语言为什么删东西,得知道它的设计者当时在被什么折磨。

2007 年,在 Google,三个人 —— Rob Pike、Ken Thompson(Unix 和 C 的共同作者)、Robert Griesemer —— 正等着一个 C++ 大项目编译完。这一等就是四十多分钟。Google 的代码库有几千万行、几千个工程师在同一片代码上协作,C++ 那套模板、多重继承、无穷无尽的语言特性,让编译慢如蜗牛、让新人读一段代码要先学会作者用了哪几种「高级技巧」。

Go 就是这三个人对着这堆痛,故意造出来的反面。它的设计目标不是「表达力最强」,而是一串很朴素的词:编译快、读得懂、好上手、适合几百人在一个巨大代码库里长期协作。为了这几个词,他们做的最重要的决定不是「加什么」,而是「拒绝加什么」

✎ 一个关于「不加」的著名故事

社区无数次请求给 Go 加泛型。Go 团队的回答不是「不行」,而是「我们还没想清楚一个值得加的设计」—— 然后他们真的忍了十年,直到 2022 年的 Go 1.18 才加,而且加得极其克制(卷 VI 会专门讲)。

对比一下:在这十年里,你熟的语言加了多少特性?这种「宁可十年不加,也不加一个会让语言变复杂的设计」的克制,就是 Go 的性格。它把「不加」当成一种功能。

「特性少」怎么会是优点

这是最反直觉的一点,值得掰开讲。你的直觉大概是:特性越多,语言越强,我能做的越多。对写你自己一个人的小项目,这大体没错。但 Go 关心的是另一个场景,在那个场景里,这个直觉会翻车。

设想一个五百人的团队、一千万行代码。每个语言特性,都是团队里每个人都得会读的东西 —— 因为你不知道下一段要维护的代码里,作者用了哪个。特性越多,「一段任意的代码里可能出现的花样」就越多,一个人要装进脑子的东西就越多,代码风格的分歧就越大(有人爱用这个技巧、有人爱用那个)。

Go 的赌注是:在大规模、长时间、多人协作这个场景下,「少一种写法」比「多一种能力」更值钱。所以它激进地做减法 —— 一种循环、一种格式化风格、一种错误处理方式、一种并发模型。你打开任意一个 Go 项目,代码看起来都惊人地像。这种「无聊」是刻意设计出来的。

GO PROVERB

「清晰胜过聪明。」

Clear is better than clever.  — Rob Pike

Rob Pike 有一组著名的 Go Proverbs(Go 谚语),这本书会时不时引一条。它们全都指向同一个方向:别炫技,别把东西写得聪明到只有你看得懂,把它写得笨一点、直一点、让下一个人一眼就懂。这不是对你能力的不信任,是对「团队和时间」这两个敌人的清醒。

gofmt:连「代码长什么样」都不让你吵

Go 把减法做到了一个你可能从没想过的地方:代码格式

在 Kotlin / Java 世界里,你们团队大概为这些吵过架:缩进用几个空格?大括号换不换行?import 怎么排序?于是有了 ktlint、有了 .editorconfig、有了一堆配置和争论。

Go 直接把这个问题删了。它自带一个 gofmt,全 Go 世界只有一种官方格式,没有任何配置项。你写得再乱,存盘时一跑 gofmt(编辑器自动做),它就变成标准样子。于是所有 Go 代码看起来都一样,你换一个项目零适应成本,团队里再没人为格式吵一个字。

GO PROVERB

「gofmt 的风格谁都不是最爱,但 gofmt 是所有人的最爱。」

Gofmt's style is no one's favorite, yet gofmt is everyone's favorite.

这句谚语精准地道破了 Go 的整个哲学:gofmt 排出来的样子,可能不是你心里最理想的那种(也许你更喜欢别的缩进),但「大家都一样、没得吵」这件事本身,比「排成你最爱的样子」值钱得多。Go 一次次做这个取舍:牺牲个人偏好,换整体一致。

所以,学 Go = 一次「卸载」

现在你该明白这本书为什么叫《留白》、为什么第一章是「卸载」了。你不是一张白纸 —— 你带着十年的 Kotlin / Java 直觉来。这些直觉里,有一部分是编程的本质(函数、递归、抽象、组合),Go 会用不同的语法把它们还给你;另一部分只是你那门语言的口音(继承的层级、异常的抛接、万物皆引用、一个表达式恨不得链十个操作符),Go 会请你把它们放下。

学 Go 的痛,几乎全在第二部分 —— 不是学新的难,是放下旧的别扭。你会一次次伸手去够那个熟悉的键,然后被提醒「这里没有」。这本书的每一章,就是陪你放下一样、并且让你心服口服地放下:看清它为什么删、你以后靠什么替代。

先看一眼整张账本

下面这台 Demo,是你接下来一整本书要逐条审判的清单。它把你从 Kotlin / Java 带来的常用特性列出来,标注 Go 对它的处理:直接删了换了完全不同的做法、还是大体保留。点上面的分类,看看比例:

数一下那个「删了」的数字 —— 9 样,接近你日常特性的一多半,被 Go 干脆利落地删掉了。再看「不一样」的 6 样:它们不是没有对应物,而是 Go 用了一种会让你一开始很不适应的方式去做同一件事(比如用组合代替继承、用接口 + 类型 switch 代替密封类)。真正「你熟的、Go 后来也补上了」的,全书只有泛型这 1 样

这张账本就是本书的地图。卷 I 收拾语法层面的减法;卷 II–III 是最费劲的重装 —— 值语义和接口,你的直觉在这里被改写得最狠;卷 IV 讲错误;卷 V 是 Go 唯一「加」回来的大礼;卷 VI 收尾并送你迁移上路。

▶ 动手

把上面的账本从头到尾读一遍,找出让你最不服气的那一条 —— 大概率是「继承没了」或「异常没了」或「三元表达式都没有?」。记住那个不服气的感觉。等你读到对应章节(继承在第 12 章、异常在第 14 章),我们会专门把这笔账算给你,看你到时候服不服。

Kotlin 对照

⇄ 同一件事,两种性格

Kotlin 的性格是「贴心」。它拼命想让你写这一行时更爽:data class 帮你生成一堆样板、扩展函数让你给别人的类加方法、?. 让你少写判空、协程让异步读起来像同步。Kotlin 站在写代码的你这一边,替你把眼前这一行削得又短又滑。

Go 的性格是「较真」。它站在读代码的所有人那一边,宁可让你现在多敲几个字、把话说全,也不许你把复杂度藏进一个聪明的简写里,留给后人去猜。

这两种性格没有绝对的对错,它们是为不同战场优化的。Kotlin 在「一个人 / 小团队 / 快速迭代 / 表达力」这一侧更舒服;Go 在「大团队 / 长期维护 / 一致性 / 一眼看懂」这一侧更稳。你要做的不是选边站,是学会在两种性格之间自由切换 —— 而这需要你先真正理解 Go 那一侧的逻辑,而不是拿 Kotlin 的舒适去嫌弃它。

这一章的一句话

Go 是一门故意做减法的语言,它删掉特性是为了让大团队在长时间里读得懂、不看错;学 Go 的过程,是把你的 Kotlin 直觉分成「本质」和「口音」两半,接过前一半、放下后一半。

下一章,我们把语法层面能一次讲完的减法一口气过一遍 —— 你会惊讶于 Go 的语法糖少到什么程度,以及这种「少」读起来是什么感觉。