你会 Kotlin,所以先学会「卸载」
你学 Go 会卡壳,几乎从来不是因为某个概念太难,而是因为你的手比脑子快 —— 你的手指头想去按 Kotlin 的那个键。想写 try,Go 说没有;想写 class Dog : Animal,Go 说没有;想 user?.name,Go 说没有。这一章不教你任何 Go 语法,它只做一件事:让你明白这些「没有」不是缺陷,是 Go 用力删出来的结果 —— 然后你才学得进去。
一个让你不适应的实验
先看两段都在打印 "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 团队反复强调的第一原则。你写一行代码只写一次,但它会被 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 项目,代码看起来都惊人地像。这种「无聊」是刻意设计出来的。
「清晰胜过聪明。」
Clear is better than clever. — Rob Pike
Rob Pike 有一组著名的 Go Proverbs(Go 谚语),这本书会时不时引一条。它们全都指向同一个方向:别炫技,别把东西写得聪明到只有你看得懂,把它写得笨一点、直一点、让下一个人一眼就懂。这不是对你能力的不信任,是对「团队和时间」这两个敌人的清醒。
gofmt:连「代码长什么样」都不让你吵
Go 把减法做到了一个你可能从没想过的地方:代码格式。
在 Kotlin / Java 世界里,你们团队大概为这些吵过架:缩进用几个空格?大括号换不换行?import 怎么排序?于是有了 ktlint、有了 .editorconfig、有了一堆配置和争论。
Go 直接把这个问题删了。它自带一个 gofmt,全 Go 世界只有一种官方格式,没有任何配置项。你写得再乱,存盘时一跑 gofmt(编辑器自动做),它就变成标准样子。于是所有 Go 代码看起来都一样,你换一个项目零适应成本,团队里再没人为格式吵一个字。
「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 的语法糖少到什么程度,以及这种「少」读起来是什么感觉。