为什么 C++ 程序员不想改用 Go 语言
下面是我在2012年六月旧金山Go SF会议上的发言。
这是一个私人谈话。我不单是对在这坐的Go开发团队成员说,我要感谢团队在推动Go发展上所做的一切。我还想感谢Go SF组织者给了我跟大家交流的机会。
几个星期前我被问道这个问题:“你被鼓励转到Go后遇到最大的惊喜是什么?”。我立刻知道了答案:虽然我们预期C++程序员会将Go当做一个替代者,然而转到Go的程序员更多来自于如Python和Ruby等语言,很少有来自C++。
Ken、Robert和我,当我们还是C++程序员时我们设计了一种新的语言来解决我们认为需要用这门新语言来解决的问题。这好像是自相矛盾的,其他C++程序员并不在乎。
今天我想来说说是什么激发我们创建 Go 语言,以及为什么得出这样的结论应该是意料之中的事情。我承诺这将是一段更倾向于 Go 语言而不是 C++ 语言的发言,所以即使你不了解 C++ 语言,你也可以从这篇文章中了解一二。
实际上,这篇文章的结论可以概括如下:你认为 less is more (少即多),还是 less is less(少即少)?
这里我用一个真实的案例作为比喻。Bell 实验室中心原来是用 3 位数字来命名的:111 是物理研究所、127 是计算科学研究所等等。在 20 世纪 80 年代,随着我们对研究的深入了解,已经不足以用 3 位数字来简单地命名研究所了。所以,我们的研究中心编号变成了 1127 。 Ron Hardin 半开玩笑地说如果我们真的了解了这个世界,我们可以去掉编号前面的 1 位,从 127 变为 27。当然,管理层不会采纳这个玩笑,也并没有被期望他们这么做。但是,我认为,Ron 的说法是有道理的。 Less can be more (精简反而有更深的内涵)。你了解得越深入,你就能够用更精炼的语言来概括。
记住这个道理。
回到 2007 年 9 月,我正在做一些为一个大型的 Google C++ 项目做一些琐碎而中心的工作。你可能接触过这样的程序,在我们大型分布式编译集群上编译这个程序就用了大概 45 分钟。后来,听说了在 C++ 标准委员会的 Google 工程师将会进行 C++0x(即现在的 C++11)新特性的演讲。
在这场 1 个小时的演讲里,我们听说了 C++0x 计划中的 35 项新特性。实际上,或许还有更多的,但演讲中只描述了其中的 35 项。当然,一些新特性可能很微小,但是演讲中的特性却都十分重要。一些特性非常精妙而难以理解,像右值引用(rvalue-reference);但同时也有很多 C++ 特色语法,像可变参数模板(variadic templates);而其他一些则非常疯狂,像用户定义数据标志(user-defined literals)此刻我问了自己一个问题:C++委员会真的相信C++因为没有足够的特性而出问题吗?当然,作为Ron Hardin玩笑话的变种,简化编程语言相比于增加语言是更大的进步。这想法是有些可笑,但先记住它。
在那次C++演讲前几个月,我曾给自己一个演讲,你可以去油Tube上看,内容是关于我在1980年间创造的一个娱乐性质的并发语言。那个语言叫做Newsqueak,当然它是Go的前驱。
我做那个演讲是因为Newsqueak中的一些想法是我在Google工作时错过的,而我又重新对这些想法进行了思考。我确信它们将使得写服务器代码更加简单,而且Google将会从中受益。
事实上我曾经尝试去寻找一个方法把这些想法引入C++,但是失败了。把并发操作和C++的控制结构连接起来太难了,反过来这样也很难看到真正的优势。再加上C++让自己看起来非常难处理,尽管我承认很难使语言真正易用。所以我遗弃了这个想法。
但C++0x演讲让我重新思考。真正困扰我的——我认为也困扰着Ken和Robert——是具有原子类型的新C++内存模型。在一个已经超负荷的类型系统中加入这样一个定义微观细节的集合,让人感觉是错的。这看起来也非常短时,因为硬件很可能在下一个十年里有明显的变化,把语言和今天的硬件结合这么紧密将是不明智的。
会议结束后,我们回到办公室。继续开始了新一轮的编译,然后转过桌子面对着 Robert,开始询问那些关注的问题。在编译结束以前,我们说服了 Ken ,决定自己动手做点什么。我们并不希望一直用 C++ 写程序,我们,特别是我,希望在为 Google 写代码的时候能够自然地使用并发编程技术。我们也希望强调“Programming in large”的方向。
我们在白板上写了很多我们想要的特性。我们从大方向出发,忽略详细的语法和语义只关注事物的主要部分。
我还保留着在那周里一个邮件列表,下面是一些节选:
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 或者 C++ 都相当不同。不同点甚至比大家注意到的多。我列举一下 Go 语言对 C/C++ 语言的关键的简化:
- 正则语法(不需要一个符号表来做语法分析)
- 垃圾回收
- 没有头文件
- 没有循环依赖
- 常量只是数字
- int 和 int32 是两种不同的类型
- 用大小写字母区别变量的可见性
- 对所有类型可用的方法(没有类的语法)
- 没有子类型继承(没有子类)
- 包层次的初始化和良好定义的初始化顺序
- 文件的编译以包组织
- 任意顺序出现的包层次全局变量
- 没有算术转换(arithmetic conversions)(通过常量支持)
- 接口是隐含的(不需要用“implements”显式声明)
- 嵌入的(没有超类的好处)
- 方法被声明为函数(不需要特殊的地方)
- 方法就是函数
- 接口是没有数据的方法
- 方法以参数名匹配,而不是通过类型
- 没有构造函数或者析构函数
- 后增量运算(x++)和后减量运算(x--)是语句而不是表达式
- 没有前增量运算和前减量运算
- 赋值操作不是表达式
- 没有序列点(sequence point)的概念:evaluation order defined in assignment, function call
- 没有指针运算
- 内存分配总是初始化为 0
- 可以获取本地变量的地址
- 在方法中没有 this 关键字
- 分段的栈结构
- 没有 const 或者其他类型注解
- 没有模板结构
- 没有异常处理结构
- string、slice 和 map 是语言结构
- 数组越界检查
尽管需要精简以及缺失部分的列表依然很长,但我相信,Go 比 C 或 C++ 更具表现力。积少可以成多。
当然,没有人可以一下子拿出所有的东西。诸如类型行为、实践中运行良好的语法以及使库间相互运行良好的不可言喻的部分等等需要慢慢积累。
我们也添加了一些 C 或 C++ 中所没有的东西,例如 slices 和 maps、复合声明,顶级文件表达式(非常庞大,且多数部分尚不为人知),反射,拉圾回收等。当然,还有原生的并发。
明显缺失的一项是类型层次。这里我可以说脏话么?
早在GO语言第一次出现的时候,就有人说,不能想象在工作中如何使用一个没有泛型的编程语言。这是我在某处作报告的时候,知道了这种奇怪的想法。
公平的说,他只是以自己的立场发表看法,或许他真的很喜欢C++中的STL为他带来的便利。所以为了讨论的目的,我们只有把他的想法看成很肤浅表面的东西。
按照那种想法,编写一个容器,例如整型的list和字符串的map,是一种不能承受的工作量。我知道了这种奇怪的想法后,花了我很少很少的编程时间实现了这些容器,即使是使用一些没有泛型的编程语言。
但更重要的是,那种想法中把类型作为解决这一难题的方法。类型,不是多态函数,不是语言基元不是其他方法的辅助,竟然是类型。
那是与我相关的细节。
从C++和Java转向Go的程序员不懂得使用类型编程的想法,尤其是继承、子类和与之相关的内容。也许关于类型我是个门外汉,但我还没发现一个模型能有如此的表现力。
我的一位老友Alain Fournier曾对我说,他把学术工作的最低层次看作是分类学。你知道吗?类型分层恰恰就是分类学。你需要决定哪块进什么箱子,每个类型的父类,无论是 A继承于B还是B继承于A。一个可排序的数组是一个排序数组或者是一个用分类机表示的数组吗?如果你相信类型地址对这些问题都进行了设计,那你就必须对这个问题做出判断。
我认为那是个可笑的方法去思考编程。重要的不是事物间的祖先关系,而是它们可以为你做什么。
当然,那就是进入Go的接口所在。但它们只是更大图景,即真正的Go哲学的一部分。
如果C++和Java是关于类型层次和类型分类的话,那Go是关于构造。
Doug McIlroy, Unix管道的发明者,在1964(!)中写道:
我们应该使用一些方法,像花园浇水软管一样来连接程序——当需要用另一种方法处理数据时,犹如拧水管一样进入另一个分支。这也是IO的方式。这也是Go的方式。Go接受了这个想法并发扬光大。它是一个构造和耦合的语言。
很明显的例子是它提供给我们构造成分的接口方式。成分是什么不重要,如果它实现了方法M,我可以把它放在这里。
另一个重要的例子是并发性如何构造那些独立执行的计算。
甚至有一个非寻常(非常简单)的类型构造形式:嵌入。
这些构造技术正是Go的风味特色,这是和C++或者Java程序的最大的区别。
===========
我想提一下Go设计中的一个不相干的方面:Go是为了写大型程序、被大型团队编写并维护而设计的。
关于“大型编程”的想法,从某种角度看C++和Java占据着这个领域。我认为这仅仅是个历史偶然现象,或者是个业界偶然现象。但大部分人都觉得这和面向对象设计有某种关系。
我一点也不相信这些。大型软件当然需要方法论,但更加需要的是强依赖性的管理、干净的接口抽象和优秀的文档工具,而这些C++都不能很好的处理(尽管Java明显好些)。
我们现在还不知道,因为还没有足够的用Go写的软件,但我相信Go将证明是一个优秀的用于大型编程的语言。时间将证明一切。
现在是时候回答我们刚开始提出的令人惊讶的问题:
为什么Go,一种完全为C++是怎样用所设计的语言,又不吸引更多的C++程序员?
玩笑撇在一边,我认为其原因是Go和C++有着完全不同的哲学。
C++是可以让你在指尖拥有一切。我在C++11FAQ中发现了如下引用:
C++抽象范围可表示的很优雅,灵活,手工制作的专业代码和零成本相比是大大增加的。
这样的想法和Go的操作方式是不同的,零成本可不是Go的目标,但至少不是CPU零成本。Go 宣称极小化编程人员的付出是它们认为最重要的。
Go 语言不是包罗万象的。Go 并不自带很多函数,不会对每一个执行细节都有精确的控制。例如,你不会有 RAII。相反地,Go 语言有一个垃圾收集器(Garbage Collector,即GC),因此,不需要一个释放内存的函数。
Go 语言给你的是一套强大且易于理解、易于构建的对问题解决方案。它不是一种像你用其他语言一样的快而复杂、或者说思想上有所激发的语言,但它几乎肯定会更容易编写、更易于阅读、更容易理解、更容易维护,也许还更安全。
用另一种方式来说,当然过于简洁:
Python和Ruby程序员转向Go,因为他们不需要屈服于大量的表达形式,但可以提升性能,并能使用并发性。
C++程序员没有转向Go,因为他们为精确控制他们的编程领域付出了很多,不想屈服与它的任一方面。对他们而言,软件并不仅仅意味着搞定工作,它意味着使用某种确定的方法去做。
那么,问题是Go的成功将与他们的世界观冲突。
我们本应该从开始时就意识到这一点。那些因为C++11的新特性而激动不已的人是不会关心一个有更少特性的语言。即使,最终它能提供更多。
谢谢。