Rob Pike谈Google Go:并发,Type System,内存管理和GC
概要
Rob Pike谈Google Go,内容涉及:无类OOP编程、Go的接口、采用Goroutines和Channels的并发特性,以及Go中帮助缩短GC间歇的一些特性。
个人简介
Rob Pike是Google的首席工程师。最近,他参与开发了Go语言。在此之前,Rob在贝尔实验室的CS研究中心工作,主要从事计算机图形,语言,并发编程等方面的工作,同时他也是Plan 9及Inferno OSes的架构师。此外,他还与Brian Kernighan合著了《The Unix Programming Environment》及《The Practice of Programming》。
关于会议
GOTO会议的目标听众是软件开发者,IT架构师以及项目经理。GOTO Aarhus是在Denmark举行的年度会议。之所以发起GOTO(以前称为JAOO),是因为Trifork的管理部门不满足于现有的一些会议,他们希望创建一个论坛,带给开发人员学习的灵感、活力和渴望,同时也给大家带来参与IT社区的机会。
Rob,你创建了Google Go这门语言。什么是Google Go?能简明扼要的介绍一下Google Go吗?
我还是讲讲为什么要创建这门语言吧,和你的问题稍有些不同。我在Google做了一个有关编程语言的系列讲座,在Youtube上有,谈及了我早期所写的一个语言,叫做Newsqueak,那是八十年代的事,非常早。在做讲座期间,我开始思考为什么Newsqueak中的一些想法在我现在以C++为主的工作环境中无法使用。而且在Google我们经常要构建非常大的程序,光构建就要花很多时间,对依赖的管理也有问题,由于链接了本来并不需要的东西,二进制程序包变得很大,链接时间很长,编译时间也很长,而且C++的工作方式有点古老,其底层实际上C,C++已经有三十年的历史了,而C则更是有四十年了。用现今的硬件做计算,有很多新东西需要考虑:多核机器、网络化、分布式系统、云计算等等。
Go的主要特点是什么?有什么重要功能?
对于大多数人来说,他们对Go的第一印象是该语言将并发性作为语言原语,这对我们处理分布式计算和多核这类东西来说非常好、也非常重要。我猜许多人会认为Go是一门简单无趣的语言,没有什么特别的东西,因为其构想看起来一目了然。但实际上不能用第一印象来判断Go。很多用过Go的人会发现它是一门非常高产而且有表现力的语言,能够解决我们编写这门语言时期望其所能解决的所有问题。
Go的编译过程很快,二进制程序包又比较小,它管理依赖的方式如同管理语言本身的东西一样。这里还有一个故事呢,但是在这里就不再展开讨论了,但是这门语言的并发性使其能够以非常简单的模式来处理非常复杂的操作及分布式计算环境。我想最重要的功能可能就是并发性了,后面我们可以谈谈该语言的类型系统,其与C++、Java这类传统面向对象类型系统的差异很大。
在我们继续话题之前,能否解释一下为什么Go编译器能达到那么快的编译速度呢?有什么法宝?
它之所以快,有两个原因。首先Go有两个编译器——两个单独的实现。一个是按照Plan 9(http://plan9.bell-labs.com/wiki/plan9/1/) 风格新写的编译器,它有自己独特的工作方式,是个全新的编译器。另一个编译器叫做GCC Go,它拥有GCC前端,这个编译器是Ian Taylor后来写的。所以Go有两个编译器,速度快是二者的共同特点,但是Plan 9风格编译器的速度是GCC Go的5倍,因为它从头到脚都是全新的,没有GCC后端,那些东西会花很多时间来产生真正的好代码。
GCC Go编译器要产生更好的代码,所以速度慢些。不过真正重要的一点是Go编译器的依赖管理特性才是其编译速度快的真正原因。如果你去看一个C或C++程序,便会发现其头文件描述了函数库、对象代码等等东西。语言本身并不强制检查依赖,每一次你都必须分析代码以便清楚你的函数是怎样的。如果你编译过程中想用另一个类的C++程序,你必须先编译它所依赖的类和头文件等等等等。如果你所编译的C++程序有许多类,并且内部相关,你可能会把同一个头文件编译数百次甚至上千次。当然,你可以用预编译头文件及其他技巧来回避之一问题。
但是语言本身并不能帮上你的忙,工具可能会让这一问题得到改善,可是最大的问题是并没有什么能保证你所编译的东西就是程序真正需要的东西。有可能你的程序包含了一个并不真正需要的头文件,但是你没办法知道,因为语言并没有强制检查。而Go有一个更加严格的依赖模型,它有一些叫做包(packages)的东西,你可以把它想象成Java类文件或着类似的东西,或者函数库什么的,虽然他们并不相同,但基本思路是一样的。关键问题是,如果这个东西依赖那个东西,而那个东西又依赖另外一个东西,比如A依赖于B,B又依赖于C,那么你必须首先编译最内层的依赖:即,你先编译C,然后编译B,最后编译A。
但是如果A依赖B,但是A并不直接依赖于C,而是存在依赖传递,那么该怎么办呢?这时所有B需要从C拿到的信息都会被放在B的对象代码里。这样,当我编译A的时候,我不需要再管C了。于是事情就非常简单了:在你编译程序时,你只需将类型信息沿着依赖关系树向上遍历即可,如果你到达树的顶端,则只需编译紧邻的依赖,而不用管其它层级的依赖了。如果你要做算术运算,你会发现在Objective-C或C++或类似的语言里,虽然只包含了一个简单的头文件,但由于依赖传递的存在,你可能会编译数十万行程序。然而在Go中,你打开一个文件,里面或许只有20行,因为其中只描述了公共接口。
如果一个依赖链里只有三个文件,Go的优势可能并不明显,但是如果你有成千上万个文件的时候,Go的速度优势会成指数增长。我们相信,如果用Go的话,我们应该能够在数秒内就编译完数百万行代码。然而如果是等量的用C++编写的程序,由于依赖管理问题,编译的开销会大得多,编译的时间将会长达若干分钟。因此,Go速度快的根源主要归功于对依赖的管理。
让我们开始聊聊Go里的类型系统吧。Go里面有结构(struct)、有类型(type),那么Go里的类型是什么?
Go里的类型与其它传统编程语言里的类型是类似的。Go里的类型有整数、字符串、struct数据结构、以及数组(array),我们称之为切片(slice),它们类似于C的数组,但更易于使用,更加固定一些。你可以声明本地类型并予以命名,然后按照通常的方式来使用。Go和面向对象方式的不同之处在于,类型只是书写数据的一种方式,方法则是一个完全独立的概念。你可以把方法放在struct上,在Go里没有类的概念,取而代之的是结构,以及为此结构声明的一些方法。
结构不能与类混为一谈。但是你也可以把方法放在数组、整数、浮点数或字符串上,实际上任何类型都可以有方法。因此,这里方法的概念比Java的方法更加泛化,在Java里方法是类的一部分,仅此而已。例如,你的整数上可以有方法,听上去似乎没什么用,但是如果你想在一个叫做Tuesday的整数常量上附加上to_string方法来打印出漂亮的星期格式;或者,你想重新格式化字符串使其能够以不同的方式打印出自己,这时你就会意识到它的作用。为什么非要把所有方法或者其它好东西都塞进类里面呢,为什么不让它们提供更广泛的服务呢?
那么这些方法只是在包内部可见喽?
非也,实际上是这样,Go只允许你在包内为你所实现的类型定义方法。我不能引入你的类型然后直接把我的方法增加进去,但是我可以使用匿名属性(anonymous field)将其包裹起来,方法可不是你想加到哪就加到哪的,你要定义类型,然后才能把方法放在上面。正因为如此,我们在包里提供了另一种封装——接口(interface),但是如果你不明白谁能为对象增加方法的严格界限,就很难理解接口。
你的意思是,我可以给int增加方法,但是必须先使用typedef吗?
你要typedef一个整数类型,起个名字,如果你正在处理一星期中的七天,可以就叫它“Day”,你可以给你所声明的类型——Day增加方法,但是你不能直接给int增加方法。因为整数类型不是你定义的,不在你的包里,它是引入的但并不在你的包中定义,这就意味着你不能给其增加方法。你不能给不在你包里定义的类型增加方法。
你们借鉴了Ruby里开放类的思想,这很有意思。Ruby的开放类实际上是可以修改类并增加新的方法,这是有破坏性的,但是你们的方法本质上是安全的,因为创建了新的东西。
它是安全可控的,而且很容易理解。最初我们觉得类型用起来可能不太方便,我们也希望像Ruby那样添加方法,但这又让接口比较难以理解。所以,我们只把方法取出来,而不是放进去,我们想不出有什么更好的办法,于是限制方法只能在本地类型上,不过这种思路确实很容易理解和使用。
你还提到了typedef,是叫typedef吧?
应该叫“type”,你所说的类型——Day的定义方式是这样“type Day int”,这样你就有一个新类型了,你可以在其上增加方法、声明变量,但这个类型不同于int,不像C那样,只是同一事物另起了个名字而已,在Go里实际上你创建了一个不同于int的新类型,叫做“Day”,它拥有int的结构特性,但却有自己的方法集。
Typedef在C里是一种预处理指令吗?【编辑注/免责申明:C语言里的typedef与预处理无关】
那实际上就是个别名,但在Go里不是别名,是新类型。
我们从底层说起吧,在Go里最小的类型是什么?
最小的类型应该是布尔类型(bool)吧。bool、int和float,然后是int32、float64之类有尺寸的类型、字符串、复杂类型,可能有遗漏,但这就是基本类型集了。你可以由这些类型构建结构、数组、映射(map),映射在Go里是内建类型不是函数库。然后我想就该是接口了,到了接口,有趣的东西才真正开始。
但是,int这样的类型是值类型对吧。
Int是值类型。在Go里,任何类型都是值类型,和C一样,所有东西都是按值调用,但是你也可以用指针。如果你想引用某样东西,可以获取其地址,这样你就有了一个指针。Go也有指针但是比C指针有更多限制,Go里的指针是安全的,因为他们是类型安全的,所以你没法欺骗编译器,而且也没有指针运算,因此,如果你有个指向某物的指针,你无法将其移到对象外,也无法欺骗编译器。
它们类似C++的引用吗?
是的,很像引用,但是你可以按照你预期的方式对它们进行写操作。而且你可以使用结构内部(如缓冲区)中间的某个地址,它和Java的引用不一样。在Java中,你必须在旁边分配一个缓冲区,这是额外的开销。在Go中,你实际上把该对象分配为结构的一部分,在同一内存块中,这对性能是非常重要的。
它是结构内部一个复合对象。
是的,如果它是值而不是指针的话,是这样。当然你也可以把指针放在结构内部和外部,但是如果你有struct A,而把struct B放在struct A里,那么stuct B就是一块内存,而不像Java那样,这也是Java性能问题的原因之一。
你提到过接口比较有趣,那下面咱们就谈谈这一部分。
Go里的接口真的非常、非常地简单。接口指明了两个不同事情:其一,它表明了类型的构思,接口类型是一个罗列了一组方法的类型,因此如果你要抽象一组方法来定义一个行为,那么就定义一个接口并声明这些方法。现在你就有了一个类型,我们就叫它接口类型吧,那么从现在起所有实现了接口中这些方法的类型——包括基本类型、结构、映射(map)或其它什么类型,都隐含符合该接口要求。其二,也是真正有意思的是,和大多数语言中的接口不同的是,Go里面没有“implements”声明。
你无须说明“我的对象实现了这个接口”,只要你定义了接口中的那些方法,它就自动实现了该接口。有些人对此感到非常担忧,依我看他们想说的是:知道自己实现(Implement)了什么接口真的很重要。如果你真想确定自己实现了什么接口,还是有技巧可以做到这一点的。但是我们的想法与此截然不同,我们的想法是你不应该考虑实现什么接口,而是应该写下要做的东西,因为你不必事前就决定要实现哪个接口。可能后来你实际上实现了某个现在你尚不知晓的接口,因为该接口还未设计出来,但是现在你已经在实现它。
后来你可能发现两个原先未曾考虑过相关性的类具有了相关性——我又用了类这个词,我思考Java太多了——两个structs都实现了一些非常有用的小子集中的相关方法,这时有办法能够操作这两个structs中的任意一个就显得非常有用了。这样你就可以声明一个接口,然后什么都不用管了,即使这些方法是在别人的代码中实现的也没问题,虽然你不能编辑这些代码。如果是Java,这些代码必须要声明实现你的接口,在某种意义上,实现是单向的。然而在Go里,实现是双向的。对于接口实际上有不少漂亮而简单的例子。
我最爱用的一个真实例子就是“Reader”,Go里有个包叫做IO,IO包里有个Reader接口,它只有一个方法,该方法是read方法的标准声明,比如从操作系统或文件中读取内容。这个接口可以被系统中任何做read系统调用的东西所实现。显然,文件、网络、缓存、解压器、解密机、管道,甚至任何想访问数据的东西,都可以给其数据提供一个Reader接口,然后想从这些资源中读取数据的任何程序都可以通过该接口达到目的。这有点像我们前面说过的Plan 9,但是用不同的方式泛化的。
与之类似,Writer也是比较好理解的另一个例子,Writer 由那些要做写操作的人来实现。那么在做格式化打印时,fpringf的第一参数不是file了,而是Writer。这样,fprintf可以给任何实现了write方法的东西做IO格式化的工作。有很多很好的例子:比如HTTP,如果你正在实现一个HTTP服务器,你仅须对connection做fprintf,便可将数据传递到客户端,不需要任何花哨的操作。你可以通过压缩器来进行写操作,你可以通过我所提到的任何东西来进行写操作:压缩器、加密机、缓存、网络连接、管道、文件,你都可以通过fprintf直接操作,因为它们都实现了write方法,因此,隐含都隐含符合writer接口要求。
某种程度上有点类似结构化类型系统(structural typing,http://en.wikipedia.org/wiki/Structural_type_system)。
不考虑它的行为的话,它是有点像结构化类型系统。不过它是完全抽象的,其意并不在拥有什么,而是能做什么。有了结构(struct)之后,就规定了其内存的样子,然后方法说明了结构的行为,再之后,接口则抽象了该结构及其它实现了相同方法的其他结构中的这些方法。这是一种鸭子类型系统(duck typing,一种动态类型系统, http://en.wikipedia.org/wiki/Duck_typing),而不是结构化类型系统。
你提到过类,但Go没有类,对吧。
Go没有类。
但是没有类怎么去写代码?
带方法的结构(stuct)很像是类。比较有意思的不同之处是,Go没有子类型继承,你必须学习Go的另类写法,Go有更强大、更有表现力的东西。不过Java程序员和C++程序员刚开始使用Go的时候会感到意外,因为他们实际上在用Go去编写Java程序或C++程序,这样的代码工作得并不好,你可以这样做,但这样就略显笨拙了。但是如果你退一步,对自己说“我该怎样用Go去编写这些东西呢?”,你会发现模式其实是不同的,用Go你可以用更短的程序来表达类似的想法,因为你不需要在所有子类里重复实现行为。这是个非常不同的环境,比你第一眼看上去的还要不同。
如果我有一些行为要实现,而且想放在多个structs里,怎么去共享这些行为?
有一个叫做匿名域的概念,也就是所谓的嵌入。其工作方式是这样:如果你有一个结构(struct),而又有一些其它东西实现了你想要的行为,你可以把这些东西嵌入到你的结构(struct)里,这样,这个结构(struct)不仅仅可以获得被嵌入者的数据还可以获得它的方法。如果你有一些公共行为,比如某些类型里都有一个name方法,在Java里的话你会认为这是一组子类(继承来的方法),在Go里,你只需拿到一个拥有name方法的类型,放在所有你要实现这个方法的结构里,它们就会自动获得name方法,而不用在每个结构里都去写这个方法。这是个很简单的例子,但有不少有趣的结构化的东西使用到了嵌入。
而且,你还可以把多个东西嵌入到一个单一结构中,你可以把它想象成多重继承,不过这会让人更加迷惑,实际在Go里它是很简单的,它只是一个集合,你可以放任何东西在里面,基本上联合了所有的方法,对每个方法集合,你只需写一行代码就可以拥有其所有行为。
如果有多重继承命名冲突的问题该怎么办?
命名冲突实际上并没什么,Go是静态处理这一问题的。其规则是,如果有多层嵌入,则最高层优先;如果同一层有两个相同的名字或相同的方法,Go会给出一个简单的静态错误。你不用自己检查,只需留意这个错误即可。命名冲突是静态检查的,而且规则非常简单,在实践中命名冲突发生的也并不多。
因为系统中没有根对象或根类,如果我想得到一个拥有不同类型的结构的列表,应该怎么办?
接口一个有意思的地方是他们只是集合,方法的集合,那么就会有空集合,没有任何方法的接口,我们称之为空接口。系统中任何东西都符合空接口的要求。空接口有点类似于Java的Object,不同之处在于,int、float和string也符合空接口,Go并不需要一个实际的类,因为Go里没有类的概念,所有东西都是统一的,这有点像void*,只不过void*是针对指针而不是值。
但是一个空接口值可以代表系统中的任何东西,非常具有普遍性。所以,如果创建一个空接口数组,实际上你就有了一个多态性容器,如果你想再把它拿出来,Go里面有类型开关,你可以在解包的时候询问里面的类型,因此可以安全的进行解包操作。
Go里有叫做Goroutines的东西,它们和coroutines有什么区别?不一样么?
Coroutines和Goroutines是不同的,它们的名字反应了这一点。我们给它起了个新名,因为有太多术语了,进程(processes)、线程(threads)、轻量级线程、弦(chords),这些东西有数不清的名字,而Goroutines也并不新鲜,同样的概念在其它系统里已经都有了。但是这个概念和前面那些名字有很大不同,我希望我们自己起名字来命名它们。Goroutine背后的含义是:它是一个coroutine,但是它在阻塞之后会转移到其它coroutine,同一线程上的其它coroutines也会转移,因此它们不会阻塞。
因此,从根本上讲Goroutines是coroutines的一个分支,可在足够多的操作线程上获得多路特性,不会有Goroutines会被其他coroutine阻塞。如果它们只是协作的话,只需一个线程即可。但是如果有很多IO操作的话,就会有许多操作系统动作,也就会有许多许多线程。但是Goroutines还是非常廉价的,它们可以有数十万之众,总体运行良好并只占用合理数量的内存,它们创建起来很廉价并有垃圾回收功能,一切都非常简单。
你提到你们使用了m:n线程模型,即m个coroutines映射到n个线程上?
对的,但是coroutines的数量和线程的数量是按照程序所做工作动态决定的。
Goroutines有用于通信的通道吗?
是的,一旦有两个独立执行的功能,如果Goroutine们要相互协作它们就需要相互对话。所以就有了通道这个概念,它实际上是一个类型消息队列,你可以用它来发送值,如果你在Goroutine中持有通道的一端,那么你可以发送类型值给另外一端,那一端则会得到想要的东西。通道有同步和异步之分,我们尽可能使用同步通道,因为同步通道的构思非常好,你可以同时进行同步和通信,所有东西运行起来都步调一致。
但是有时由于效率原因或调度原因,对消息进行缓存也是有意义的。你可以向通道发送整型消息、字符串、结构、指向结构的指针等任何东西,非常有意思的事,你可以在通道上发送另一个通道。这样,我就能够把与他人的通信发送给你,这是非常有意思的概念。
你提到你们有缓存的同步通道和异步通道。
不对,同步是没有缓存的;异步和缓存是一个意思,因为有了缓存,我才能把值放在缓存的空间里进行保存。但是如果没有缓存,我必须等着别人把值拿走,因此无缓存和同步是一个意思。
每个Goroutine就像是一个小的线程,可以这么给读者解释吧。
对,但是轻量级的。
它们是轻量级的。但是每个线程同样都预分配栈空间,因而它们非常耗费资,Goroutines是怎么处理的呢?
没错,Goroutines在被创建的时候,只有非常小的一个栈——4K,可能有点小吧,这个栈是在堆中的,当然,你知道如果在C语言里有这么一个小栈会发生什么,当你调用函数或分配数组之类的东西时,程序会马上溢出。在Go里则不会发生这样的事情,每个函数的开头都会有若干指令以检查栈指针是否达到其界限,如果到达界限,它会链接到其它块上,这种连接的栈叫做分段栈,如果你使用了比刚开始启动时更多的栈,你就有了这种栈块链接串,我们称之为分段栈。
由于只有若干指令,这种机制非常廉价。当然,你可以分配多个栈块,但是Go编译器更倾向于将大的东西移到堆上,因此实际上典型的用法是,你必须在达到4K边界之前调用几个方法,虽然这并不经常发生。但是有一点很重要:它们创建起来很廉价,因为仅有一次内存分配,而且分配的内存非常小,在创建一个新的Goroutine时你不用指明栈的尺寸,这是很好的一种抽象,你根本不用担心栈的大小问题。之后,栈会随需求增长或缩小,你不用担心递归会有问题,你也不用担心大的缓存或任何对程序员完全不可见的东西,一切由Go语言来打理,这是一门语言的整体构思。
我们再来谈谈自动化方面的东西,最初你们是将Go语言作为系统级语言来推广的,一个有趣的选择是使用了垃圾回收器,但是它速度并不快或者说有垃圾回收间歇问题,如果用它写一个操作系统的话,这是非常烦人的。你们是怎么看这一问题的?
我认为这是个非常难的问题,我们也还没有解决它,我们的垃圾回收器可以工作,但是有一些延迟问题,垃圾回收器可能会停顿,但是我们的看法是,我们相信尽管这是一个研究课题,虽还没解决但是我们正在努力。对于现今的并行机,通过把机器内核的一些碎片专门分给作为后台任务的垃圾回收来进行并行回收是可行的。在这一领域有很多工作要做,也取得了不少成功,但这是个很微妙的问题,我不认为而我们会把延迟降为0,但是我相信我们可以让延迟尽可能低,这样对于绝大多数系统软件来讲它不再是个问题。我不保证每个程序都不会有显著延迟,但是我想我们可以获得成功,而且这是Go语言中一个比较活跃的领域。
有没有方法能够避免直面垃圾回收器,比如用一些大容量缓存,我们可以把数据扔进去。
Go可以让你深入到内存布局,你可以分配自己的空间,如果你想的话可以自己做内存管理。虽然没有alloc和free方法,但是你可以声明一个缓存把东西放进去,这个技巧可用来避免产生不必要的垃圾。就像在C语言一样,在C里,如果你老是malloc和free,代价很大。因此,你分配一个对象数组并把它们链接在一起,形成一个链表,管理你自己的空间,而且还不用malloc和free,那么速度会很快。你可以做与Go所做相同的事情,因为Go赋予你与底层事物安全打交道的能力,因此不用欺骗类型系统来达到目的,你实际上可以自己来做。
前面我表达了这样的观点,在Java里,无论何时你在结构里嵌入其它东西,都是通过指针来实现的,但在Go里你可以把它放在一个单一结构中。因此如果你有一些需要若干缓存的数据结构,你可以把缓存放在结构的内存里,这不仅意味着高效(因为你不用间接得到缓存),而且还意味着单一结构可以在一步之内进行内存分配与垃圾回收。这样开销就会减少。因此,如果你考虑一下垃圾回收的实际情况,当你正在设计性能要求不高的东西时,你不应该总是考虑这个问题。但如果是高性能要求的,考虑到内存布局,尽管Go是具有真正垃圾回收特性的语言,它还是给了你工具,让你自己来控制有多少内存和产生了的垃圾。我想这是很多人容易忽略的。
最后一个问题:Go是系统级语言还是应用级语言?
我们是把他设计为一种系统级语言,因为我们在Google所做的工作是系统级的,对吧?Web服务器和数据库系统、以及存储系统等,这些都是系统。但不是操作系统,我不知道Go是否能成为一个好的操作系统语言,但是也不能说它不会成为这样的语言。有趣的是由于我们设计语言时所采用的方法,Go最终成为了一个非常好的通用语言,这有点出乎我们意料。我想大多数用户并没有实际从系统观点来考虑过它,尽管很多人做过一点Web服务器或类似东西。
Go用来做很多应用类的东西也非常不错,它将会有更好的函数库,越来越多的工具以及一些Go更有用的东西,Go是一个非常好的通用语言,它是我用过的最高产的语言。
Rob Pike谈论为什么Google要开发新的Go语言
Rob Pike总是会在恰当的地方做事。在80年代初期,他在贝尔实验室与Brian Kernighan以及Ken Thompson一起工作,与Kernighan合写了“UNIX编程环境”,并与Thompson合作开发了UTF-8 字节编码标准。Pike现在是Google的一个首席工程师,并与人合作开发了Go,一个新的编程语言。Pike将在下月的OSCON大会上讨论Go,他也在下面的采访中谈论了Go的开发以及编程语言的当前状态。
创建Go语言的动机是什么?
Rob Pike:几年以前,我们在Google的几个人开始对软件开发过程感到沮丧,尤其是在使用C++编写大型的服务器软件时更是如此。我们发现二进制文件变得越来越大。他们需要花费太多的时间去编译。此语言几乎是当前世界上主要的系统软件语言,其本身已经是一种非常老旧的语言了。最近二十年来许多在硬件上的思想和变化都未能影响C++。于是我们决定着手从新设计设计一种能解决我们问题的语言:我们需要快速地构建软件,使其能在现代的多核心硬件和网络环境中运行良好,并且做到快乐编程。
尽管我们针对一些特定的问题来设定Go的目标,它已经变成一个比我们最初的设想更加通用和适用的编程语言。因此我们现在正使用它做许多不同的事情。我认为它应该在多个方向上都有美好的未来。
使用Go编程会感觉如何呢?
Rob Pike:Go具有动态语言如Python、Ruby或JavaScript的味儿,但它同时具有像Java、C或C++类语言的性能和安全性。这样你会感觉就像在使用一个轻量级的现代的脚本动态语言,但同时获得了传统语言的健壮性和性能。
Go是否具有一个强大的开发环境?
Rob Pike:我们有一套与此语言配合使用的有趣的工具。其中发布版本的标准库就有完整性分析器。因此依据问题的复杂程度,你可以编写你自己的工具,也许在使用已有库时只需要一页代码。
有一些工具可以使你与已有的库连接。对于大型的如OpenGL一类的包,你最好是仅仅连接使用已有的东西。我们可以使用我们的封装工具来做这些事,另外SWIG支持使得我们能与C++连接。但所有的基类库都是由Go编写的。
已经有了针对Eclipse以及其他环境的插件。但还需要更多的工作,我们尚没有一个IDE,我们已经有了做这些事情的一些想法。
世界上还需要另外一个编程语言吗?
Rob Pike:这是一个关于语言的愉快时光,因为已经产生了如此众多的语言。60年代末到70年代初是语言开发的爆发期,接下来则一切归于平静。这并不是说没有出现新的语言,而是语言的设计似乎都无功而返。然而在最近的5到10年,又一次出现了复兴。我将在OSCON上谈论这种现象的原因之一,便是当前常用的语言并不能满足人们的需求。 这里有新语言的生存之所,在这些方面Java、C、C++、JavaScript甚至Python都不能很好的满足需求。
Google与贝尔实验室比起来怎么样呢?
Rob Pike:这两方面都已经发生了很多改变。当我在贝尔实验室工作时,我们做了许多研究驱动的、出版导向的事情。在很大程度上,此公司不理解开源。当我到Google后,方向则变得非常不同。我们完全是一个试图让事情实现的公司。至少在后来,开源变成了公司文化的一个基本部分。因此在这方面他们是非常不同的。
随着一天天的工作进展,我认为他们有许多共通点。他们都是令人向往的工作地点,他们都具有许多聪明的人们。但在文化上,他们是通讯公司和互联网公司的差别。他们从根本上是两回事。
大道至简
http://www.linuxeden.com/html/softuse/20120702/126645.html
这是我(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 的新特性而兴奋的人们是不会在意一个没有这么多特性的语言。即便最后发现这个语言能够比他们所想象的提供更多。
谢谢大家。
我的看法
目标其实很清晰,想让google内部的工程师都用这个,代替c/c++。不知道现在google内部对GO语言是什么样的状况。
就我了解的而言,我觉得GO主要要解决的是大规模并发系统编程的问题:
1. 编译速度:这种大系统还是很有影响的,GO通过不同的编译器和依赖关系管理来提升编译速度。
2. 关于抽象:C++/Java是通过类体系来描述抽象,GO没有类主要是用接口和组合,这一点上和OO设计原则是一致的。
3. 语言层面支持并行:就像c可以实现多态,但远远比不上c++实现的多态那么好维护,语言层面支持的东西还是很有优势的。
4. 内存管理:我有点怕的是它支持内存管理,垃圾回收,可能是多虑了,其实自己管理内存在大规模程序上确实是个问题。无论如何,GO也表明这个问题是个难题,同时它也支持自己管理内存。
暂时我就这么认为了,我觉得GO是大规模并发系统这种需求催生出来的语言,所以应用也在这个领域,还很年轻有待成熟,内存垃圾回收也是比较难的一个课题,最大的优势是并发。
据说在google,youtube,豆瓣等有实际应用,它们可能都是这种场景。
元芳,你怎么看?