Ready? Go! 上篇:大道至简

本文分两部分连载于2012年5月和6月的《程序员》杂志。当时Go语言刚刚推出第一个稳定版:Go 1。刊载时略有删改。


Go语言是Google于2009年推出的静态编译型语言,旨在为开发人员提供类似Python,Ruby一样简洁的语言环境,同时又具备C/C++一样的运行效率。作为一个开源项目,在过去的两年多时间里,Go以社区协作的形式,不断地完善语言和标准库的设计与实现。最终于今年三月28日发布了第一个稳定的发行版本:Go version 1,简称Go 1。Go 1的推出,意味着Go语言和它的标准库已经进入了一个稳定阶段。对于谨慎的开发人员来说,开发Go程序正趁当下,现在已经可以放心地开发Go程序,不必再考虑未来语法和标准库的变化。


正如Go官方所说,Go 1的目的是发行一个稳定的Go语言实现,而非全盘修改。所以对于已经熟悉Go的开发人员来说,Go 1与之前的版本并没有很大差异。绝大部分修改都是对标准库命名空间的再组织。本系列文章包含上下两篇,上篇重点讨论Go的开发背景,部分语法和类型系统;下篇讨论Go的并发模型和工具链。


本文的全部代码可以在github上找到。

编译效率,运行效率,开发效率

曾经那些代码,品读起来,恰似满腹经论的学者之间,细语轻声,拂琴畅谈。绝非乌烟瘴气之下,面红耳赤地与编译器争辩。--- Dick P. Gabriel


按照Go官方FAQ的说法,Go的出现是为了弥补其他语言在系统级开发上的缺陷。这样一句话,难免有人会觉得,Go的诞生纯粹是几位计算机界大佬 --- Go最初的核心开发人员包括Robert Griesemer, Rob Pike, Ken Thompson --- 在埋怨自己的不肖后辈,并在吐槽同时,自己亲自操刀开发了这样一个语言。但更中肯地说法,恐怕是他们在目睹和体验了Google的系统级开发之后,总结出的一套在异构,分布式多核系统之上的生存之道。而今天Google所面临的问题,也许恰恰是几年后每个公司的面试题目。与其说Go是一座空中楼阁,不如说是各位系统开发界大佬进入新时代后的一部略带辛酸的开发史。


Go的基本设计理念是:编译效率,运行效率和开发效率要三者兼顾。使用Go开发,要让开发人员感觉到Python的便利,C/C++的运行效率,以及小到可以被忽略的编译时间。为了实现这个理念,形成了Go语言的以下几个特性:

  • 编译,静态类型语言。由此可以提供满足对运行效率敏感的系统级应用。

  • 垃圾回收,去除复杂的内存释放工作。

  • 简洁的符号和语法,极力减少开发人员输入的字符数。

  • 平坦的类型系统,去除了复杂的继承关系。使用结构化类型系统(Structural type system),既简化了事前设计工作,也为未来增加抽象层提供了非侵入式的解决方法。

  • 基于CSP模型的并行,简化了并发结构之间的通信和数据共享。为多核时代的程序开发打好基础。

  • 比线程更轻量的goroutine,让一个线程可以执行多个并发结构。不必使用异步通信,就足以达到线程池与select/poll/epoll的效果。极大简化了多连接的开发。

  • 使用一套简单的规范,开发人员不必再单独编写脚本指定依赖关系和编译流程。仅仅使用代码本身和go工具链,就可以处理各种依赖关系。写完代码,一条命令,自动下来各种依赖,直接编译/安装。无需make,autoconf,automake,setup.py等工具支持。

前两点应该是不言自明的。本系列文章重点对后五点做详细分析。本篇后半部分将讨论语法和类型系统;下篇将讨论并发模型和工具链。关于如何提高编译效率,由于涉及较多编译器实现细节,在此不讨论。


化繁为简,语法当先

public static <I, O> ListenableFuture<O> chain (ListenableFuture<I>input, Function<? super I, ? extends ListenableFuture<? extends O>>function) 苍天啊!大地啊!快把这货拦下来吧!--- 来自某聊天记录


初学Go,会让人感到它神似C语言,并非是其背后强大的开发团队和他们与C语言千丝万缕的联系,也不仅仅是Go对系统级开发的重视和它类C的语法。而是简洁与实用并存的语法让人触目难忘。在21世纪,一个严肃的通用编程语言,使用一份仅有约两万词的语言规范,只定义25个关键字,42个操作符,却涵盖了并发,面向对象等方方面面,仅仅这些,对于开发人员来说,这个语言怕也足以值得一试了。


这里列举部分语法:

  • 没有分号了。其实Go语言规范中,Go中的语句的确是以分号做分割的。但Go语言规定,词法分析器使用一套规则自动地添加分号。这种对词法分析器的要求,带来了一条编码规范:左大括号不要单独写在一行,否则词法分析器可能会产生不必要的分号。虽然多了这样一条编码规范,但是带来的结果是开发人员确实不必考虑分号了。以Go标准库为例,其中没有任何一个语句,需要开发人员明确地写下分号。

  • 使用:=操作符声明变量和其初始值,不必明确指明变量类型,因为初始值已经说明了变量的类型。试想要声明一个结构/类。使用Java需要foo.Foo a = new foo.Foo;, 而Go则只需a := new(foo.Foo)。对于比较长的类型名来说,这可以减少很多打字。需要注意的一点是,:=操作符同时完成了变量声明和赋值的操作。如果变量之前已经声明,只是要给它赋值,则依然使用常见的=赋值。

  • for,if的判断条件,和switch的控制语句无需括号。也就是说,可以写成if a < b { dosomething() }。还可以写switch b { case 1: dosomething() }。

  • 所有循环只有for一个关键字。而for循环有可以有几种写法: 和C语言for循环类似的写法for i := 0; i < 10; i++ { dosomething() };和C语言while循环类似的写法for i < 100 { dosomething() };以及无条件循环for { iteration() };另外,对于切片类型(类似于Java中的数组,是一种线性结构)和map类型(go中的哈希表实现),还可以配合range关键字,遍历存储的成员,如for i, e := range list { dosomething(i, e) }。

  • 直观简单的访问控制。只有名字以大写字母开头的变量,函数/方法,或结构,才能被包(package)外代码访问。否则只是包内可见。这不仅简化了访问控制,而且对开发者来说,只需要看到名字,不必去找声明,就可以知道访问条件。

  • switch的条件可以是表达式且可以没有控制语句。这意味着以下语句是合法的:

    switch {

    case i % 2 == 0:

        process_even(i)

    case i % 2 != 0:

        process_odd(i)

    }

这些看似不经意的改变,却有效的简化了程序的书写和阅读。让开发者减少打字之外,也让读程序变得更加简单。由于这些改变和特性并不复杂,在此不多加介绍。


正交原则:数据,方法,复用和抽象

早知[C++]如此[复杂],要是能穿越回去,我们肯定会搞一个面向对象版本的C语言出来。--- Ken Thompson,UNIX创始人,Go语言作者,在接受采访时关于Go和面向对象。


这一节主要讨论Go的类型系统。和C语言一样,Go也使用结构体进行数据抽象:

type Duck struct {

    Name string

}

接下来,可以为这个结构定义一个方法:

func (d *Duck) Eat() {

    fmt.Println(d.Name, 

        "Duck is having dinner!")

}

与C++,Java的类不同,为结构体添加方法不必在声明结构体的时候就声明该方法。只需要保证方法与结构体定义在同一个包(package)中。由此数据与行为被分离,在设计数据抽象(结构体)阶段,不必考虑具体哪些行为。func关键字后面的(d *Duck)表示这个方法从属于哪个类型。d这个变量被成为“接收者”(receiver),在方法的定义中,d的使用类似C++/Java中的this指针。


然后,我们可以使用这个结构体和它的方法了:

func main() {

    d := new(Duck)

    d.Name = "Donald"

    d.Eat()

}

按照讲述面向对象语言的规律,下一步应该介绍继承机制了。Go的回答很简单:没有继承。这听起来似乎不可理喻,但却带来了直接的好处:极大地简化了类型系统。一方面,编译器可以更高效了;另一方面,开发人员不必事前考虑各种复杂的继承关系。


但是,继承的好处也非常明显的:第一,复用,子类可以直接继承父类的方法的实现,减少了重复代码;第二,多态,开发人员可以使用更抽象的表示(父类)调用具体的实现(子类),由此可以编写通用的代码在抽象层次上进行操作。

Go如果只是粗鲁地去掉继承机制,而不去面对继承所要解决的问题,显然是不明智的。为此,Go分别使用两套机制来实现继承要达到的效果:匿名成员实现代码复用;接口类型实现类型抽象。


实现代码复用,Go在结构体定义中,引入了“匿名成员”(Anonymous Field)的概念。了解面向对象设计的开发者一定听说过,要优先使用对象组合而非继承的方式来实现代码复用的原则。Go的匿名成员,实际也就是一种对象组合。

继续上面的例子,假如需要定义一个DonaldDuck类型,它的Eat方法实现和Duck一样,但是多了一个叫做Age的成员:

type DonaldDuck struct {

    Duck

    Age int

}

可以看到,仅仅是把Duck这个类型名字放在结构体的定义中。由于并没有显示地给出这个成员的名字,由此得名“匿名成员”。这个写法本质上是定义了另外的一个结构体,它包含了一个类型为Duck,名字也叫做Duck成员(是的,这个成员名和类型名是一样的)。同时,也包含了一个类型为整数,名为Age的成员。


那么这匿名成员究竟对代码复用有什么意义呢?Go对匿名成员有一条特殊规则:包含匿名成员的结构体也具有了匿名成员类型的方法。简单说,对于上面的例子,DonaldDuck中包含了匿名成员Duck,那么就好像DonaldDuck实现了Duck的各种方法。这样,下面的代码就容易理解了:

func main() {

    d := new(DonaldDuck)

    d.Name = "Donald"

    d.Age = 10

    d.Eat()

}

我们首先需要申请一个DonaldDuck类型的对象,然后对这个对象的各个成员进行赋值,最后调用Eat()方法。至今为止,我们看到,使用匿名成员可以实现像继承一样的代码复用。但是比起继承,它又少了点东西。比如,你无法将一个DonaldDuck类型的对象地址赋值给一个Duck类型的指针。你也不能在DonaldDuck中重新定义Eat方法的实现。简单说来,匿名成员仅仅在语法层面上做了一些简化,并没有触及任何类型系统的内容。对于类型系统来说,DonaldDuck和Duck完全是两个不同类型。


为了实现继承机制的另外一部分,即多态,Go引入了接口类型的概念。与Java中的接口类似,Go的接口也是声明了一组方法的原型,然后由具体结构体(类)的方法来实现各种接口。但是与Java不同的是,Go使用了类似OCaml的结构化类型系统(Structural Type System),这种类型系统不要求实现接口的类型显示地声明究竟要实现哪些接口。只需要定义好与接口类型一致的全部方法,就说该类型实现了这个接口。具体说来,对于以上代码,我们可以定义一个Animal的接口:

type Animal interface {

    Eat()

}

这个接口中仅仅定义了一个方法:Eat,它没有任何输入参数,也没有返回值。至此为止,Duck和DonaldDuck都是Animal这个接口的实现。没错,你不必修改Duck和DonaldDuck的代码,不必显示的写明implements Animal,只要实现了Eat方法,并且原型与接口类型中定义的一致就可以了。那么,我们就可以直接声明一个Duck类型的指针,然后把它赋值给Animal接口类型的变量:

func main() {

    var a Animal

    d := new(Duck)

    d.Name = "Don"

    a = d 

    a.Eat()

}


如果还要哪些类型实现Animal这个接口,只需要为这些类型实现Eat方法,就可以了。


这种接口类型为开发人员提供了方便的“先实现,后抽象”的机制。因为接口本身是非侵入式的,不必在定义结构的时候就明确说明要实现哪些接口。开发人员可以在实现了一些结构之后,再寻找它们之间通用的接口表示形式,进而定义一个接口类型。这时候,任何实现了接口中指定方法的类型,都已经是这个接口的实现了。不必再修改以前运行良好的代码,也极大地简化了开发流程。试想,在几万行代码中,寻找几个实现了某些方法的类型,哪怕只在每个类型的定义中添加两个单词,也足以让人生厌了。


这种“先实现,后抽象”的机制也符合我们认识世界的一般规律。哪怕是自己写的代码,往往也是在使用一段时间后,才发现它们背后的一些抽象表示。单纯地要求在代码开发之前的设计阶段就定义好良好的类型层次,是一种无视人类认识局限的荒谬做法。Go作为一种实用的语言,承认这种局限的同时,为人们提供了最小修改代价的方案。


Go的类型系统体现了Go的另一个设计原则:正交原则。线性代数中,正交意味着一组向量之间没有任何一个可以投影到其他向量上。引申到程序设计领域,就是任何特性只针对一个问题,并且互相之间没有交集。具体到Go的类型系统,则体现在方法与数据的分离;和复用与抽象的分离。这样的分离使得开发者可以通过分析问题本身,有效地针对问题选择不同特性。


小结

本篇重点讨论了Go语言的语法和类型系统。强调了Go的一些基本设计原则。这一切都是以简洁为基础,试图以最小的语言特性覆盖各种常见问题,这酷似当年的C语言。尽管这样的设计也许不会迎来多少学术界的赞誉,却足以把开发者从复杂的语法设计,层层的类型关系中拯救出来,用更清晰简单的方法解决手头的问题。

但只有这些还不足以令Go续写C语言的辉煌。它必须要正视它所处时代必须面对的问题,这就是多核带来的挑战。如今的程序,不能再指望仅仅提高在单核上的运行效率,而提高整个程序的效率。面对多核时代,开发者必须要发掘程序中潜在的并行结构,最大化利用多核的并行能力。而Go则为开发者提供好了称手工具,去迎接多核带来的新挑战。下期我们将重点讨论Go语言中对于并行结构的处理,与go工具链。敬请关注。


--------------------------------------------------------------------------------------

欢迎关注码术!



本文来自:CSDN博客

感谢作者:codemanship

查看原文:Ready? Go! 上篇:大道至简(转)

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。