原文 Less is exponentially more 是 Rob Pike 自己整理的他在六月22日,旧金山的 Golang 会议上的演讲稿。清晰的介绍了 Go 的前世今生,来龙去脉。为了让更多的人能够更加清楚的认识到 Go 的优雅并喜爱上 Go,特翻译成中文,以飧读者。
—————-翻译分隔线—————-
大道至简
这是我(Rob Pike)在 2012 年六月,旧金山 Go 会议上的演讲内容。
这是一个私人演讲。我并未代表 Go 项目团队的任何人在此演讲,但我首先要感谢团队为 Go 的诞生和发展所做的一切。同时,我也要感谢旧金山 Go 社区给我这个演讲机会。
在几个星期之前我被问到,“在推出 Go 之后,什么令你感到最为惊奇?”我立刻有了一个答案:尽管我们希望 C++ 程序员来了解 Go 并作为一个可选的语言,但是更多的 Go 程序员来自如于 Python、Ruby。只有很少来自 C++。
我们——Ken,Robert 和我自己曾经是 C++ 程序员,我们设计新的语言是为了解决那些我们编写的软件中遇到的问题。而这些问题,其他 C++ 程序员似乎并不怎么在意,这看起来有些矛盾。
今天我想要谈谈是什么促使我们创建了 Go,以及为什么本不应该是这样会我们惊讶的结果。我承诺讨论 Go 会比讨论 C++ 多,即便你不了解 C++ 也仍然完全跟得上主题。
答案可以概括为:你认为少既是多,还是少就是少?
这里有一个真实的故事作为隐喻。贝尔实验室最初用三个数字标识:111 表示物理研究,127 表示计算机科学研究,等等。在上世纪八十年代早期,一篇如期而至的备忘录声明由于我们所了解的研究正在增长,为了便于识别我们的工作,必须再添加一位数。因此,我们的中心变为 1127。Ron Hardin 开玩笑半认真的说道,如果我们真的更好的了解了这个世界,我们可以减少一位数,使得 127 仅为 27。当然管理层没有听到这个笑话,又或者他们不愿意听到,但是我想这其中确有大的智慧。少既是多。你理解得越好,越含蓄。
请务必记住这个思路。
回到 2007 年 9 月,我在一个巨大的 Google C++ 程序(就是你们都用过的那个)上做一些琐碎但是很核心的工作,我在那个巨大的分布式集群上需要花大约 45 分钟进行编译。收到一个通知说 Google 雇佣的一对为 C++ 标准化委员会工作的夫妇将会做一场报告。收到一个通知说几个受雇于 Google 的为 C++ 标准化委员会工作的人将会做一场报告。他们将向我们介绍那时还被称作 C++0x(就是现在众所周知的 C++11)中将会有哪些改进。
在长达一个小时的报告中,我们听说了诸如有已经在计划中的 35 个特性之类的事情。事实上有更多,但仅有 35 个特性在报告中进行了描述。当然一些特性很小,但是意义重大,值得在报告中提出。一些非常微妙和难以理解,如左右值引用(rvalue references),还有一些是 C++ 特有的,如可变参数模板(variadic templates),还有一些就是发疯,如用户定义数据标识(user-defined literals)。
这时我问了自己一个问题:C++ 委员会真得相信 C++ 的问题在于没有足够的特性?肯定的说,在另一个 Ron Hardin 的玩笑中,简化语言的成就远远大于添加功能。当然这有点可笑,不过请务必记住这个思路。
就在这个 C++ 报告会的数月前,我自己也进行了一场演讲,你可以在 YouTube 上看到,关于我在上世纪 80 年代开发的一个玩具性质的并发语言。这个语言被叫做 Newsqueak,它是 Go 的前辈了。
我进行这次报告是因为在 Newsqueak 中缺失的一些想法,在为 Google 工作的时候我再次思考了这些它们。我当时确信它们可以使得编写服务端代码变得更加轻松,使得 Google 能从中获得收益。
事实上我曾尝试在 C++ 中实现这些思路,但是失败了。要将 C++ 控制结构和并发操作联系起来太困难了,最终这导致很难看到真正的优势。虽然我承认我从未真正熟练的使用 C++,但是纯粹的 C++ 仍然让所有事情看起来过于笨重。所以我放弃了这个想法。
但是那场 C++0x 报告让我再次思考这个问题。有一件令我十分困扰的事情(同时我相信也在困扰着 Ken 和 Robert)是新的 C++ 内存模型有原子类型。感觉上在一个已经负担过重的类型系统上加入如此微观的描述细节的集合是绝对的错误。这同样是目光短浅的,几乎能确信硬件在接下来的十年中将迅速发展,将语言和当今的硬件结合的过于紧密是非常愚蠢的。
在报告后我们回到了办公室。我启动了另一个编译,将椅子转向 Robert,然后开始沟通关键的问题。在编译结束前,我们已经把 Ken 拉了进来,并且决定做些什么。我们不准备继续写 C++ 了,并且我们——尤其是我,希望在写 Google 代码的时候能够做轻松的编写并发。同时我们也想勇往直前的驾驭“大编程”,后面会谈到。
我们在白板上写了一堆想要的东西,和其必要条件。忽略了语法和语义细节,设想了蓝图和全局。
我这里还有那时的一个令人神混魂颠倒的邮件。这里摘录了一部分:
Robert: 起点:C,修复一些明显的缺陷,移除杂物,添加一些缺失的特性。
Rob: 命名:“go”。你们可以编造这个名字的来由,不过它有很好的底子。它很短,容易拼写。工具:goc, gol, goa。如果有交互式调试器/解释器,可以就叫做“go”。扩展名是 .go。
Robert 空接口:interface {}。它们实现了所有的接口,所以这个可以用来代替 void *。
我们并没有正确描绘全部的东西。例如,描绘 array 和 slice 用了差不多一年的时间。但是这个语言特色的大多数重要的东西都在开始的几天里确定下来。
注意 Robert 说 C 是起点,而不是 C++。我不确定,不过我相信他是指 C,尤其是 Ken 在的情况下。不过事实是,最终我们没有从 C 作为起点。我们从头开始,仅仅借鉴了如运算符、括号、大括号、和部分关键字。(当然也从我们知道的其他语言中吸取了精髓。) 无论如何,我们现在同 C++ 做着相反的事情,解构全部,回到原点重新开始。我们并未尝试去设计一个更好的 C++,甚至更好的 C。仅仅是一个对于我们在意的那种类型的软件来说更好的语言。
最终,它成为了一个与 C 和 C++ 完全不同的语言。每个发布版本都越来越不同。我制作了一个 Go 中对 C 和 C++ 进行的重要简化的清单:
- 规范的语法(无需用于解析的符号表)
- 垃圾收集(唯一)
- 没有头文件
- 明确依赖
- 无循环依赖
- 常量只能为数字
- int 和 int32 是不同的类型
- 字母大小写设定可见性
- 任何类型都可以有方法(没有类)
- 没有子类型继承(没有子类)
- 包级别初始化和定义好的初始化顺序
- 文件编译到一个包中
- 包级别的全局表达与顺序无关
- 没有算术转换(常量做了辅助处理)
- 隐式的接口实现(无需“implements”定义)
- 嵌入(没有向父类的升级)
- 方法如同函数一样进行定义(没有特的别位置要求)
- 方法就是函数
- 接口仅仅包含方法(没有数据)
- 方法仅通过名字匹配(而不是通过类型)
- 没有构造或者析构方法
- 后自增和后自减是语句,而不是表达式
- 没有前自增或前自减
- 赋值不是表达式
- 按照赋值、函数调用定义时的顺序执行(没有“sequence point”)
- 没有指针运算
- 内存总是零值初始化
- 对局部变量取地址合法
- 方法没有“this”
- 分段的堆栈
- 没有静态或其他类型注解
- 没有模板
- 没有异常
- 内建 string、slice、map
- 数组边界检查
除了这个简化清单和一些未提及的琐碎内容,我相信,Go 相比 C 或者 C++ 是更加有表达力的。少既是多。
但是即便这样也不能丢掉所有东西。仍然需要构建类型工作的方式,在实践中恰当的语法,以及让库的交互更好这种令人感到忌讳不可言喻的事情。
我们也添加了一些 C 或者 C++ 没有的东西,例如 slice 和 map,复合声明,每个文件的顶级表达式(一个差点被忘记的重要东西),反射,垃圾收集,等等。当然,还有并发。
当然明显缺少的是类型层次化。请允许我对此爆那么几句粗口。
在 Go 最初的版本中,有人告诉我他无法想像用一个没有范型的语言来工作。就像之前在某些地方提到过的,我认为这绝对是神奇的评论。
公平的说,他可能正在用其自己的方式来表达非常喜欢 STL 在 C++ 中为他做的事情。在辩论的前提下,让我们先相信他的观点。
他说编写像 int 列表或 map string 这样的容器是一个无法忍受的负担。我觉得这是个神奇的观点。即便是那些没有范型的语言,我也只会花费很少的时间在这些问题上。
但是更重要的是,他说类型是放下这些负担的解决途径。类型。不是函数多态,不是语言基础,或者其他协助,仅仅用类型。
这就是卡住我的细节问题。
从 C++ 和 Java 转过来 Go 的程序员怀念工作在类型上的编程方式,尤其是继承和子类,以及所有相关的内容。可能对于类型来说,我是门外汉,不过我真得从未发现这个模型十分具有表达力。
我已故的朋友 Alain Fournier 有一次告诉我说他认为学术的最低级形式就是分类。那么你知道吗?类型层次化就是分类。你必须对哪块进哪个盒子作出决策,包括每个类型的父级,不论是 A 继承自 B,还是 B 继承自 A。一个可排序的数组是一个排序过的数组还是一个数组表达的排序器?如果你坚信所有问题都是由类型驱动设计的,那么你就必须作出决策。
我相信这样思考编程是荒谬可笑的。核心不是东西之间的祖宗关系,而是它们可以为你做什么。
当然,这就是接口进入 Go 的地方。但是它们已经是蓝图的一部分,那是真正的 Go 哲学。
如果说 C++ 和 Java 是关于类型继承和类型分类的,Go 就是关于组合的。
Unix pipe 的最终发明人 Doug McIlroy 在 1964 (!) 这样写到:
我们应当像连接花园里的龙头和软管一样,用某种方式一段一段的将消息数据连接起来。这同样是 IO 使用的办法。
这也是 Go 使用的办法。Go 用了这个主意,并且将其向前推进了一大步。这是一个关于组合与连接的语言。
一个显而易见的例子就是接口为我们提供的组合元件的方式。只要它实现了方法 M,就可以放在合适的地方,而不关心它到底是什么东西。
另一个重要的例子是并发如何连接独立运行的计算。
并且也有一个不同寻常(却非常简单)的类型组合模式:嵌入。
这就是 Go 特有的组合技术,滋味与 C++ 或 Java 程序完全不同。
===========
有一个与此无关的 Go 设计我想要提一下:Go 被设计用于帮助编写大程序,由大团队编写和维护。
有一个观点叫做“大编程”,不知怎么回事 C++ 和 Java 主宰了这个领域。我相信这只是一个历史的失误,或者是一个工业化的事故。但是一个广泛被接受的信念是面向对象的设计可以做些事情。
我完全不相信那个。大软件确实需要方法论保驾护航,但是用不着如此强的依赖管理和如此清晰的接口抽象,甚至如此华丽的文档工具,但它不比强大的依赖管理、清晰的接口抽象和优秀的文档工具来得更重要,而这些没有一样是 C++ 做好的事情(尽管 Java 明显做得更好一些)。
我们还不知道,因为没有足够的软件采用 Go 来编写,不过我有自信 Go 将在大编程领域脱颖而出。时间证明一切。
===========
现在,回到我演讲一开始提到的那个令人惊奇的问题:
为什么 Go,一个被设计为用于摧毁 C++ 的语言,并为并未获得 C++ 程序员的芳心?
撇开玩笑不说,我认为那是因为 Go 和 C++ 有着完全不同的哲学。
C++ 是让你的指尖解决所有的问题。我在 C++11 的 FAQ 上引用了这段内容:
C++ 与那些巨大增长的特别编写的手工代码相比,具有更加广泛的抽象,优雅、灵活并且零成本的表达能力。
这个思考的方向与 Go 的不同。零成本不是目标,至少不是零 CPU 成本。Go 的主张更多考虑的是最小化程序员的工作量。
Go 不是无所不包的。你无法通过内建获得所有东西。你无法精确控制每个细微的执行。例如没有 RAII。而可以用垃圾收集作为代替。也没有内存释放函数。
你得到的是功能强大,但是容易理解的,容易用来构建一些用于连接组合解决问题的模块。这可能最终不像你使用其他语言编写的解决方案那么快,那么精致,在思想体系上那么明确,但它确实会更加容易编写,容易阅读,容易理解,容易维护,并且更加安全。
换句话说,当然,有些过于简单:
Python 和 Ruby 程序员转到 Go 是因为他们并未放弃太多的表达能力,但是获得了性能,并且与并发共舞。
C++ 程序员无法转到 Go 是因为他们经过艰辛的战斗才获得对其语言的精确控制能力,而且也不想放弃任何已经获得的东西。对于他们,软件不仅仅是关于让工作完成,而是关于用一个确定的方式完成。
那么,问题是,Go 的成功能否反驳他们的世界观。
我们应当在一开始的时候就意识到了一点。那些为 C++11 的新特性而兴奋的人们是不会在意一个没有这么多特性的语言。即便最后发现这个语言能够比他们所想象的提供更多。
谢谢大家。
感谢 Leo Jay、fango、techabc、spin6lock、panovr、lihui、许大、幻の上帝 的订正。根据 Monnand 的建议,将标题修改为《大道至简》
有疑问加站长微信联系(非本文作者)