Go语言简介

Go语言是狗狗(Google)员工Robert Griesemer, Rob Pike, 和Ken Thompson在2007年提出来的一种编程语言。这几个人中,Ken Thompson这个名字对从事计算机专业的人来说是如雷贯耳。他是Unix的始祖,同时,也是B语言的发明者。B语言对C语言有非常重要的影响。至于C语言,你要不知道的话都不好意思说自己是计算机专业的。发明C语言的Dennis Ritchie跟Ken同是贝尔实验室的同事,同时,Dennis也是Unix的重要贡献者。Rob也在贝尔实验室的Unix团队工作过。他参与了多种操作系统和编程语言的发明。现在互联网上流行的UTF-8(一种字符编码方式)就是Rob和Ken一起发明的。总之,都是一帮牛人。

这帮牛人为什么要发明Go呢?Robert提到他们几个对C++都深恶痛绝,认为它太复杂,缺乏并发,扩展性差,编译慢,所以几个人凑在一起就搞了这个Go语言。2009年狗狗正式宣布Go语言,2012年Go语言1.0版正式发布,到目前已经是1.8版了。

按照计算机大牛C. A. R. Hoare的说法,编程语言设计者的任务不是“创新”,而是“合并”。Hoare这里所说的不要创新,更多的是指不要重新发明轮子(Reinventing the wheel),或者一味地追求新奇。他更强调的是继承和改进。从总体看,Go语言比较好地体现了这一思想,它有很重的C语言的痕迹,同时,又有Pascal这类语言的一些特征。它不是一个面向对象(Object Oriented)的语言,没有泛型(generics)。Robert等认为C++等面向对象语言复杂的一个理由就是面向对象语言和泛型提供的动态类型的能力导致大型程序的可维护性很差。Go语言通过接口(Interface)提供了面向对象的一些能力。业界对Go语言没有提供泛型,有很多争论。有的人痛心疾首,认为没有泛型的语言简直就不能被称为“现代”编程语言。有的觉得不是大问题。就我们使用的经验,没有泛型确实在某些时候会觉得不方便,但总体影响不大。

Go语言有很多特性,比如高并发能力,内存垃圾收集,接口,静态类型和动态类型,通道,内置哈希表,数据片等数据结构等,下面我对一些主要特性做些介绍。

Go语言语法比较简单,学过C语言的一般很快可以上手。下面是一个Go语言的“Hello World”程序:

package main

import "fmt"

func main() {
    fmt.Println("Hello, World")
}

一个Go程序由一个或多个包组成。每个包由一个或多个文件组成。一个Go程序文件由三部分组成。第一部分定义这个文件所属的包,这里我们可以看到这个文件属于一个叫main的包;第二部分是定义这个程序文件引用的所有的外部包,这里,我们只用了一个叫“fmt”的外部包。第三部分定义常数,数据类型,变量,和数据,这里的例子很简单,只有一个叫main的函数,这是每个Go程序都必须有的,它是所有Go程序的入口,也就是说,程序的执行是从这开始的。我们这个程序只做了一件事:在标准输出设备(屏幕)上输出一行字:Hello, World。

不像Python,Javascript等动态类型的语言,Go 语言跟C语言类似,是一个静态类型语言。也就是说,所有变量,函数返回值等都必须有固定的类型,并且,在运行过程中,类型是不能被改变。这个特性对程序的可维护性很重要,很多程序错误可以在编译的时候被发现。Python程序有时候让人很困惑,因为你很难确定一个变量的类型。

Go语言并不要求显式定义所有变量的类型,它可以根据数据来推导出变量的类型,比如,我们可以这样定义一个初始值为“student”的string类型的变量:

    var s string = "student"

也可以:

    var s = "student"

或者:

    s := "student"

接口(Interface)是一种非常强大的编程机制。Go语言的接口与Java的接口类似,但更灵活:实现一个接口不要求implement关键字,只要实现了一个接口的所有方法,就可以说实现了这个接口。这个特点对于从Java转过来学Go的朋友来说有点不习惯,但很快我们可以发现这样做的好处,比如: type MySpecialInt int func (m MySpecialInt) String() string { return fmt.Sprintf(“Hello! I’m %d!”, int(m)) } 这个例子为MySpecialInt类型定义了一个叫String()的方法,这个方法的返回值是string。在fmt包中,Stringer接口就是一个String()的方法,所以,我们这里就实现了fmt的Stringer接口。那么,以下程序的输出是什么呢?

package main

import (
    "fmt"
)

type MySpecialInt int
func (m MySpecialInt) String() string {
    return fmt.Sprintf("Hello! I'm %d!", int(m))
}

func main() {
    var i MySpecialInt = 10
    
    fmt.Println(i)
}

这个程序的输出是:

Hello! I'm 10!

也就是说,只要实现了String() 这个方法,我们就可以定制fmt的输出结果。这是Go语言不需要显式地说明它实现了何类接口带来的好处。

在Go语言中,你可以不需要预先定义接口类型,比如以下函数中就直接使用的一个interface{ T() }的接口类型:

    func x(y interface{ T() }) { y.T() }

Go语言允许空接口,也就是没有定义任何方法的接口。这也意味着,任何一个类型都实现了空接口。在空接口中可以存储任何类型的值,比如以下程序中describe()函数的参数是空接口类型的,它可以接受任何类型的值:

package main

import "fmt"

func main() {
    var i interface{}
    describe(i)

    i = 42
    describe(i)

    i = "hello"
    describe(i)
}

func describe(i interface{}) {
    fmt.Printf("(%v, %T)\n", i, i)
}

这个程序的运行结果是:

(<nil>, <nil>)
(42, int)
(hello, string)

细心的朋友看到上面程序可能会问,Go不是静态类型语言吗,变量类型不能变,为什么上面的程序i的值可以是整数,又可以是字符串类型?这是Go接口类型变量的一种特性:接口类型变量实际上既有静态类型,又有动态类型。接口类型变量的静态类型就是这个变量定义时的接口类型,在这个例子中,i的静态类型是interface{};接口类型变量的动态类型是在运行过程中给这个变量赋的值,这这个例子里刚开始是整数类型,然后是字符串类型。

接下来大家就要问了,如果接口类型的变量的动态类型在运行过程中可以变的话,我怎么知道当前的值的动态类型是什么?Go提供了类型断言(Type assertion)来解决这个问题。

_config.yml

Go的类型断言是通过“.(T)”或者“.(type)”这样的语法来完成。以上例子的输出是:

hello
hello true
0 false
panic: interface conversion: interface {} is string, not float64

goroutine 1 [running]:
main.main()
    /tmp/sandbox498287889/main.go:17 +0x3a0

注意第17行会引起panic,因为i的动态类型是string,不是float64。同样的断言,第14行没有panic是因为该行用一个bool变量ok来接收断言是否合法的结果。所以,在使用断言是,如果不确定动态类型,最好还是采用第14行的方式。

Go语言的并发性(Concurrency)是它最强大的能力之一。这个并发是通过goroutine来实现的。Goroutine是一种轻量的,可以跟其它Goroutine并发执行的函数。Goroutine的开销很小,主要开销是栈。但栈的初始空间可以很小,然后根据需要分配扩大。我们可以把Goroutine理解成一种轻量级的线程,但这个线程不是内核线程。内核线程是重量级的。Go 的运行时间调度器会把Goroutine放到内核线程中,如果一个内核线程被阻隔了,调度器会把相关的Goroutine放到其它内核线程,这样保证了Goroutine的高并发性。起一个Goroutine很简单,就是 go后面跟函数名,比如以下程序起了一个叫say的Goroutine,同时又调用了say这个函数。这两段代码会并发执行。

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

写C/C++语言程序时最大痛苦之一是内存的分配和释放。在并发程序里这个问题更严重。Go 语言运行时间能支持自动内存垃圾回收,大大减轻了程序员的负担。Go采用的是三色并行mark-and-sweep算法。到Go1.8版本,性能大为提高。以后有机会我会专门描述一下Go的垃圾回收机制。

Go 还有很多特性,比如,Channel,Defer,Map,Slice等,限于篇幅,就不细讲了。另外,Go 语言程序是一个静态链接的可执行文件,不依赖其它动态库,内存要求低。相对Java来说,没有JVM的要求,发布和部署都很方便。

基于Go的以上优点,加上出生于狗狗,它一出来就得到很多关注。业界许多项目,比如,Docker,Kubernetes等都是使用Go来编写。相信未来Go的使用会越来越广泛。

Written on April 18, 2017