1. Go概述

Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.

上面是Go官方对于Go这门新的编程语言的说明。其实我也才接触Go一周多时间,之前我使用最多的是C语言,很多地方都说“Go是21世纪的C语言”,我觉得这种说法虽不是非常严谨,但却是给别人介绍Go最简单直观的方式了。我觉得Go吸收了很多程序语言的优点,不光是C。我目前对Go的感觉就是:它是一种提供了底层操作的面向对象的现代程序语言,它没有非常复杂的语法,但却可以比较容易的写出高效的现代化程序,特别是在并发方面。一言以蔽之,就是非常棒。好了,开始正题吧。

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello, 世界!")
}

上面这个就是Go版的Hello World程序。我们用它来简单介绍Go程序的结构,具体的细节后面慢慢介绍。

首先就是package,Go中的package类似于其他语言中的库或者模块,作用也类似,主要是为了代码功能模块化、封装、分开编译、重用等。一个package通常由同一目录下的一个或多个.go文件组成,所有这些文件定义了这个包的功能。同一个包内的对象在本包内都是可以访问的,和他们在不在同一个文件里面没有关系。不考虑代码组织的话,其实同一个包的东西其实可以放在一个go文件里面的。

其次就是import语句了。import这个应该比较容易理解,就是引入其他包,类似于C里面的#include和java里面的import等。不过,Go中的import语句必须跟在package语句后面,且所有import进来的包都必须使用,未使用的话就会编译不通过(而且,在Go中定义的变量也必须使用,否则会报错)。当然,go提供了一个工具golang.org/x/tools/goimports,它可以自动帮我们插入和删除我们引用和未使用的package。当然,当我们引入很多包的时候,不同的包里面可能会有同名的对象,Go提供了别名的技术来解决这个问题,后面再介绍。

最后就是main函数了。当然,main永远是最特殊的。package main不同于其他package,它不是一个库,它定义一个可执行的程序。main函数也不同于其他函数,它是程序执行的起点。

虽然这个程序很简单,但它已经是一个完整的Go程序了,再复杂的Go程序无非就是多import了一些包,多定义了一些函数,多使用了一些语法而已。但框架仍然是这样。

最后我们再介绍一些Go中的“黑科技”。

  1. 代码风格。Go语句结尾不需要;(分号),当然你加上也无所谓。但如果你想将多条语句写在一行的话,那就必须加了。其实是Go编译器帮我们把这个事做了。解析Go代码的时候,编译器会把换行符(newline)转换为分号。这样,我们使用换行符时可要小心了。比如,上面的例子中,我们千万不能把main后面的{(左花括号)放到下一行去。同理,我们写x+y这种语句时,也千万不能在+号前面加换行。是不是感觉一不小心就会出错?不用担心。Go提供了一个工具gofmt,它可以对我们的代码进行格式化,使用方法也非常简单。
  2. init函数func init() { /* ... */}。这个函数也比较特殊,不能被调用或者引用。在这之前我们先说一下程序的初始化顺序:先初始化包级别(package-level)的变量,当然被import进来的包要先初始化,有依赖顺序的先初始化被依赖的。现在我们来说这个init函数,每一个go文件里面可以有多个(也可以没有)init函数,当程序启动的时候,这些init函数就按照他们的声明顺序依次执行。等包级的初始化以及init都执行完以后,才转到main函数执行。某种程序上,init有点像构造函数。
  3. 对象的大小写决定它的可见性。在Go中,大写字母开头的是被导出的(exported),也就是说在包外可以访问的;而非大写字母开头的对象只能在本包内访问。我觉得可以简单理解为一个package就是一个class(当然Go中没有类),大写字母开头的对象都是public的,非大写字母开头的都是private的。
  4. 函数可以有多个返回值(后续会介绍)。

2. 名字

Go中对象(函数、变量、常量、类型)的名字(Names)的规则和很多其他语言一样:必须以字母或者下划线开头,由字母、数字、下划线构成。需要注意的是Go天然使用Unicode,所以这里的字母是Unicode范围内的字母,而不仅仅是ASCII码范围内的字母。

Go有以下25个关键字,这些关键字不能用作名字:

break    default    func    interface    select     
case     defer      go      map          struct
chan     else       goto    package      switch     
const    if         range   type         fallthrough
for      import     return  var          continue 

除了关键字外,Go还有一些内部已经使用的名字:

  • 常量(Constants)
true    false    itoa    nil
  • 类型(Types)
int    int8    int16    int32    int64
uint   uint8   uint16   uint32   uint64
float32    float64    complex64    complex128
bool   type    rune    string    error
  • 函数(Functions)
make    len    cap    new    append    copy    close    delete
complex    real    imag    panic    recover

注意:

  1. 内部已经使用的名字不同于关键字,它仍然可以作为你自己起的名字,比如你可以定义一个变量叫new。但最好不要这样做,阅读代码时容易产生混淆。
  2. 在Go中,名字首字母的大小写有着特殊的含义:首字母大写的名字是被导出的(exported),被导出的对象(变量、函数等)在包外也可以引用(包的概念文章后面会讲到)。可以简单理解为首字母大写的对象是public的,而非大写的是private的。

3. 变量

3.1 变量声明

Go中主要包含四种声明(Declarations):var(变量声明),const(常量声明),type(类型声明),func(函数声明),这四个都是关键字。

变量声明的格式为:var name type = expression(对应中文:var 名字 类型 = 表达式

不过我们可以省略type= expression二者之一。如果省略类型,那变量的类型根据表达式确定;如果省略了表达式,那变量的值就是对应类型的零值——数字类型的零值是0,bool型的零值是false,string的零值是"",interface和其他引用类型(slice、pointer、map、channel、function)的零值是nil,符合类型的零值(比如struct和array)是它们每个成员对应的零值。所以,在Go中不存在很多其他语言中的未初始化的变量。

看下面几个例子:

var s string     // s = ""
var i, j, k int  // int,int,int
var b, f, s = true, 2.3, "four" // bool,float64, string
var f, err = os.Open(name)    // os.Open returns a file and an error

3.2 短变量声明

短变量声明(Short Variable Declarations)格式为:name := expression。看几个例子:

i := 100  // 等效于 var i = 100
i, j := 0, 1
f, err := os.Open(name)

短变量声明格式相比于上面的声明更加简洁一些,但是它只能用于函数中,而上面的声明可以用于任何地方。

但对于短变量声明有以下注意点:

  • 注意区分:=是声明,而=是赋值。
i, j := 0, 1    // 声明变量i和j    
i, j = j, i     // 给变量i和j赋值,这种赋值叫tuple assignment
  • 短变量声明不必声明所有的变量。什么意思呢?看下面的例子,第一条语句中声明了in和err变量。而第二条语句中其实只声明了out变量,err只是赋值而已,因为err之前已经声明过了。
in, err := os.Open(infile)
//...
out, err := os.Create(outfile)
  • 短变量声明必须至少声明一个新变量。这又是啥意思?看下面的例子,第一条语句中声明了f和err变量,而第二条语句中没有增加新变量,只是赋值所以不能使用:=,应该使用=
f, err := os.Open(infile)
// ...
f, err := os.Create(outfile)  // compile error:no new variable. 应该改为f, err = os.Create(outfile)

3.3 指针

指针(Pointers)和C中的指针没有什么大的区别。但有一点重要的变化:在函数中返回局部变量的地址是OK的。函数返回后,我们依旧可以使用函数返回的指针去访问函数之前的那个局部变量。

var p = f()    // p = 1
func f() *int {
    v := 1
    return &v    // OK
}

3.4 new函数

我们也可以使用new函数创建一个变量:new(T)创建一个T类型的未命名变量(unnamed variable),并将它初始化为T类型的零值,返回它的地址(即*T)。比如:

p := new(int)    // p是指向int类型的指针,指向new出来的变量的地址
*p = 2    // 将未命名变量值设为2

在Go中,new用的比较少,往往只是为了少定义一个临时变量。而且new创建的变量的内存不一定分配在堆上,这往往取决于变量的生命周期(见2.4的例子)。

3.5 变量的生命周期

Go中变量的生命周期(Lifetime)极大部分和其他语言一样:包级别(package-level)的变量在程序运行的整个生命周期都有效,而局部变量只在自己的块内有效。但是这并不是绝对的,这和变量离开它的作用域后是否仍然可以访问以及Go的GC有关系。看下面的例子:

exp1:
var global *int
func f() {
    var x int
    x = 1
    global = &x
}

exp2:
func g() {
    y := new(int)
    *y = 1
}

exp1中,因为x离开函数f后仍然可以访问(通过global指针),所以它的内存是在堆(heap)上分配的,我们称之为x逃离了f(x escapes from f)。而exp2中,离开函数g后,*y就无法访问了,所以虽然y使用new创建的变量,编译器仍然可以在栈(stack)上面给它分配空间。这也是和C语言的一个区别。

因为Go有自己的GC,所以我们一般不用太关注变量的生命周期。

4. 赋值

关于赋值,没有很多特殊之处。主要有两个注意点:

(1)Go中的函数可以返回多个值,这样使用多返回值的函数给变量赋值的时候,左边的变量个数必须和函数个数一致。如果我们对其中某个返回值不感兴趣,可以使用_(下划线)。这个规则除了适用于函数外,还适用于:map查询(map lookup)、类型断言(type assertion)、信道接收(channel receive)。

f, err = os.Open("foo.txt")
v, ok = m[key]
v, ok = x.(T)
v, ok = <-ch

_, err = io.Copy(dst, src)
_, ok = x.(T)

关于这个概念,需要和range关键字做一下区分:for ... range用于遍历 array, slice,string,map, or channel.也返回两个值:

Range expression1st value2nd value (optional)notes
array or slice a [n]E, *[n]E, or []Eindex i inta[i] E
string s string typeindex i intrune intrange iterates over Unicode code points, not bytes
map m map[K]Vkey k Kvalue m[k] V
channel c chan Eelement e Enone

如果range的左边只有一个变量的时候,那么这个变量的值是1st value。当然我们也可以使用_,但是range是可以省略第二个变量的。

(2)两个对象可以比较的充要条件就是它们可以相互赋值。