编程语言的选择已经非常之多。偏系统级的语言有 C,C++,Java,C#,Delphi,Objective-C等;偏快速业务开发的语言有PHP,Python,Perl,Ruby,JavaScript,Lua等;面向特定领域的,有 R,Matlab等。那么,为什么我们需要 Go这样一门新语言呢?

在2000年前的单机时代,C语言是编程之王。随着机器性能的提升,软件规模与复杂度的提高,Java逐步取代了C的位置。

尽管看起来Java已经深获人心,但Java编程的体验并未尽如人意。历年来的编程语言排行榜显示,Java语言的市场份额在逐步下跌,并趋近于C语言的水平,显示了这门语言的后劲不足。如下图所示:

Go语言官方自称Go是一门系统级语言。之所以开发Go 语言,是因为“近10年左右开发程序之难让我们有点沮丧”。Go语言这一定位暗示了Go希望取代C和Java地位,成为最流行的语言。

Go希望成为互联网时代的C语言。多数的系统级语言包括Java、C#,其根本的编程哲学来源于C++,将C++的面向对象进一步发扬光大。但是Go语言的作者们很清楚,C++ 真的没啥好学的,他们要学的是C语言。C语言经久不衰的根源是它足够简单。Go语言也要足够得简单。

那么,互联网时代的C语言需要具备哪些特性呢?

首先,并行与分布式支持。多核化和集群化是互联网时代的典型特征。作为一个互联网时代的C语言,必须要让这门语言操作多核与计算机集群如单机一样容易。

其次,软件工程支持。工程规模不断扩大是产业发展的必然趋势。单机时代语言可以只关心问题本身的解决。互联网时代的C语言还需要考虑软件品质保障、团队协作相关的话题。

最后,编程哲学的重塑。计算机软件发展经历了数十年的发展,形成了面向对象等多种学术流派 。 什么才是最佳的编程实践?作为互联网时代的C语言,需要回答这个问题。

接下来我们来聊聊Go语言是如何来同时做到这些特性的。

并发与分布式

多核化和集群化是互联网时代的典型特征。语言需要哪些特性来应对多核化和集群化呢?

第一个话题是并发执行的“执行体”。执行体是个抽象的概念,在操作系统层面有多个概念与之对应,比如操作系统自己掌管的进程(Process)、进程内的线程(Thread)、以及进程内的协程(Coroutine),它也叫轻量级线程 。 多数语言在语法层面并不直接支持Coroutine,而通过库的方式支持的Coroutine功能也并不完整,比如仅仅提供Coroutine的创建、销毁与切换等能力。如果在这样的Coroutine 中调用一个同步IO操作,比如网络通讯、本地文件读写,都会阻塞其他的并发执行Coroutine 。从而无法真正达到Coroutine本身期望达到的目标。

Go 语言在语言级别支持 Coroutine, 叫goroutine。 Go语言标准库提供的所有系统调用(Syscall)操作,当然也就包括了所有同步IO操作,都会出让CPU给其他goroutine。这让事情变得非常简单。 我们以Java和Go做对比,近距离观摩下两者对“执行体”的支持。

为了简化,在样例中用的是Java标准库中的Thread,而不是Coroutine。如下:

public class MyThread implements Runnable {

    String arg;

    public MyThread(String a) {
        arg = a;
    }

public void run() {
    ...
}

public static void main(String[] args) {
    new Thread(new MyThread("test")).start();
    ...
}
}

相同功能的代码,在Go语言中是这样的:

func run(arg string) {
    ...
}

func main() {
    go run("test")
...
}

我相信你已经明白为什么Go语言会叫Go语言了:Go语言献给这个时代最好的礼物,就是加了go这个关键字。当然也有人会说叫Go语言是因为它是Google出的。好吧,这也是个不错的闲聊主题。

第二个话题是“执行体间的通讯”。执行体间的通讯包含几个方式:
• 执行体之间的互斥与同步
• 执行体之间的消息传递

先说“执行体之间的互斥与同步”。当执行体之间存在共享资源(一般是共享内存)时,为保证内存访问逻辑的确定性,需要对访问该共享资源的相关执行体进行互斥。当多个执行体之间的逻辑存在时序上的依赖时,也往往需要对执行体之间进行同步。互斥与同步是执行体间最基础的交互方式。

多数语言在库层面提供了Thread间的互斥与同步支持。那么Coroutine之间的互斥与同步呢?呃,不好意思,没有。事实上多数语言标准库中连Coroutine都是看不到的。

再说“执行体之间的消息传递”。在并发编程模型的选择上,有两个流派,一个是共享内存模型,一个是消息传递模型。多数传统语言选择了前者,少数语言选择后者,其中最典型的代表是Erlang语言。业界有专门的术语叫“Erlang风格的并发模型”,其主体思想是两点:一是“轻量级的进程(Erlang中的进程这个术语就是我们上文说的执行体)”,二是“消息乃进程间通讯的唯一方式”。当执行体之间需要相互传递消息时,通常需要基于一个消息队列(Message Queue),或者进程邮箱(Process Mail Box)这样的设施进行通讯。

Go语言推荐采用“Erlang风格的并发模型”的编程范式,尽管传统的“共享内存模型”仍然被保留,允许适度地使用。在Go中内置了消息队列(Message Queue)的支持,只不过它叫channel。两个goroutine之间可以通过channel来进行交互。

软件工程

单机时代语言可以只关心问题本身的解决,但是随着工程规模不断扩大,软件复杂度不断增加,软件工程也是语言设计层面要考虑的重要课题。多数软件需要一个团队共同去完成。在团队协作的过程中,人们需要建立统一的交互语言来降低沟通的成本。规范化体现在多个层面,如:

• 代码风格的规范。
• 错误处理规范。
• 包管理。
• 契约的规范(接口)。 • 单元测试规范。
• 功能开发的流程规范。
• ... Go语言很可能是第一个将代码风格强制统一的语言。例如Go语言要求public的变量必须大写开头,private变量则小写开头。这种做法不仅免除了 public、private关键字,更重要的是统一了命名风格。 另外,Go语言对 { } 应该怎么写进行了强制,比如以下风格是正确的:

if expression {
    ...
}

但下面这个写法就是错误的:

if expression 
{
    ...
}    

而C/Java语言中则对花括号的位置没有任何要求。这个见仁见智。但很显然的是,所有的Go代码的花括号位置肯定是非常统一了。

最有意思的其实还是 Go 语言首创的错误处理规范:

f, err := os.Open(filename)
if err != nil {
    log.Println("Open file failed:", err)
    return
   }
    defer f.Close() 
  ... // 操作f这个打开的文件

这里有两个关键点。其一是defer关键字。defer语句的含义是不管程序是否出现异常,均在函数退出时自动执行相关代码。在上面的例子,正是有了这个defer才使得无论后续是否会出现异常都可以确保文件被正确关闭。其二是Go语言的函数允许返回多个值。所有可能出错的函数,建议最后一个返回值为error类型。error类型只是一个系统内置的interface。如下:

type error interface {
    Error() string
}

有了error类型,程序出现错误的逻辑就看起来相当统一。

在Java中你可能这样写代码来保证资源的正确释放:

Connection conn = ...;
try {
    Statement stmt = ...;
    try {
        ResultSet rset = ...;
        try {
            ... // 正常代码
        }
        finally {
            rset.close();
        }
    }
    finally {
        stmt.close();
    }
}
finally {
    conn.close();
}

完成同样的功能,相应的Go代码只需要写成这样:

conn := ...  
defer conn.Close()

    stmt := ...  
    defer stmt.Close()
rset := ...  
defer rset.Close()
... // 正常代码

对比两段代码,Go语言错误处理的优势显而易见。当然其实Go带给我们的惊喜还有很多。后续有机会我们可以就某个更具体的话题详细展开来谈一谈。

编程哲学

计算机软件发展经历了数十年的发展,形成了多种学术流派,有面向过程编程、面向对象编程,函数式编程、面向消息编程等等。这些思想究竟孰优孰劣,众说纷纭。

C语言是纯过程式的,这和它产生的历史背景有关。Java语言则是激进的面向对象主义推崇者,典型表现是它不能容忍体系里中存在孤立的函数。而Go语言没有去否认任何一方,而是用批判吸收的眼光,将所有编程思想做了一次梳理,融合众家之长,但时刻警惕特性复杂化,极力维持语言特性的简洁,力求小而精。

从编程范式角度来说,Go语言是变革派,而不是改良派。

对于C++、Java、C# 等语言为代表的面向对象思想体系,Go语言总体来说持保守态度,有限吸收。 首先,Go语言反对函数和操作符重载(overload)。而C++、Java、C# 都允许出现同名函数或操作符,只要他们参数列表不同。虽然重载解决了一小部分OOP的问题,但同样给这些语言带来了极大的负担。Go语言相比有着完全不同的设计哲学。既然函数重载带来了负担,并且这个特性并不对解决任何问题有显著的价值,那么Go就不提供它。

其次,Go语言支持类、类成员方法、类的组合。但反对继承,反对虚函数(virtual function)和虚函数重载(override)。确切地说,Go也提供了继承,但是采用了组合的文法:

type Foo struct {
    Base
    ...
}

func (foo *Foo) Bar() {
    ...
}

再次,Go语言也放弃了构造函数(constructor)和析构函数(destructor)。由于Go语言中没有虚函数,也就没有 vptr,支持构造函数、析构函数就没有太大的价值。本着“如果一个特性并不对解决任何问题有显著的价值,那么Go就不提供它”的原则,构造函数和析构函数就这样被Go语言的作者们干掉了。 在放弃了大量的OOP特性后,Go语言送上了一份非常棒的礼物:接口(interface)。你可能会说,除了C这么原始的语言外,还有什么语言没有接口呢?是的,多数语言都有提供接口(interface)。但它们的接口都不同于Go语言的接口。

Go语言中的接口与其他语言最大的一点区别是它的非侵入性。在C++/Java/C# 中,为了实现一个接口,你需要从该接口继承:

class Foo implements IFoo { // Java 文法
    ...
}

class Foo : public IFoo { // C++ 文法

    ...
}

IFoo* foo = new Foo;

在Go语言中,实现类的时候无需从接口派生:

type Foo struct { // Go 文法
    ...
}

 var foo IFoo = new(Foo)

只要 Foo 实现了接口 IFoo 要求的所有方法,就实现了该接口,可以进行赋值。

Go语言的非侵入式接口,看似只是做了很小的文法调整,但实则影响深远。

其一,Go语言的标准库,再也不需要绘制类库的继承树图。你只需要知道这个类实现了哪些方法,每个方法是啥含义就足够了。

其二,不用再纠结接口需要拆得多细才合理。比如我们实现了File类,它有这些方法:

Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
Seek(off int64, whence int) (pos int64, err error)
Close() error

那么,到底是应该定义一个 IFile 接口,还是应该定义一系列的 IReader, IWriter, ISeeker, ICloser 接口,然后让 File 从他们派生好呢?事实上,脱离了实际的用户场景,讨论这两个设计哪个更好并无意义。问题在于,实现File类的时候,我怎么知道外部会如何用它呢?

其三,不用为了实现一个接口而import一个包,目的仅仅是引用其中的某个interface的定义。在Go语言中,只要两个接口拥有相同的方法列表,那么他们就是等同的,可以相互赋值。例如:

package one

    type ReadWriter interface {
        Read(buf []byte) (n int, err error)
        Write(buf []byte) (n int, err error)
    }

package two

  type IStream interface {
  Write(buf []byte) (n int, err error)
  Read(buf []byte) (n int, err error)
  }

这里我们定义了两个接口,一个叫 one.ReadWriter,一个叫 two.IStream。两者都定义了Read、Write方法,只是定义的次序相反。one.ReadWriter先定义了Read再定义Write,而two.IStream反之。

在Go语言中,这两个接口实际上并无区别。因为:
• 任何实现了 one.ReadWriter 接口的类,均实现了 two.IStream。
• 任何 one.ReadWriter 接口对象可赋值给 two.IStream,反之亦然。
• 在任何地方使用one.ReadWriter接口,和使用two.IStream并无差异。

所以在Go语言中,为了引用另一个package中的interface而import它,这是不被推荐的。因为多引用一个外部的package,就意味着更多的耦合。

除了面向对象编程(OOP)外,近年出现了一些小众的编程哲学。Go语言对这些思想亦有所吸收。例如,Go语言接受了函数式编程的一些想法,支持匿名函数与闭包。再如,Go语言接受了Erlang语言为代表的面向消息编程思想,支持goroutine和channel,并推荐使用消息而不是共享内存来进行并发编程。总体来说,Go语言是一个非常现代化的语言,精小但却非常强大。

总结

在十余年的技术生涯中,我接触过,使用过,喜爱过不同的语言,但总体而言,Go语言的出现是最让我兴奋的事情。我个人对未来10年编程语言排行榜的趋势判断如下:

• Java语言的份额继续下滑,并最终被C和Go语言超越。
• C语言将长居编程榜第二的位置,并有望在Go取代Java前重获得语言榜第一的宝座。
• Go语言最终会取代Java,居于编程榜之首。

本书将尽可能的展现出Go语言的迷人魅力,让更多人能够理解这门语言,热爱这门语言, 让这门优秀的语言能够落到实处,把程序员从以往繁复的语言细节中解放出来,集中精力于开发更加优秀的系统软件。