Go

Table of Contents

1 Go语言简介

Go, also commonly referred to as golang, is a programming language developed at Google in 2007 by Robert Griesemer, Rob Pike, and Ken Thompson. The language was announced in November 2009.

Go is a compiled, statically typed language in the tradition of Algol and C, with garbage collection, limited structural typing, memory safety features and CSP-style concurrent programming features added.

参考:
An Introduction to Programming in Go: https://www.golang-book.com/books/intro (较好的入门资料)
Go home page: https://golang.org/
Go For CPP Programmers: https://github.com/golang/go/wiki/GoForCPPProgrammers
Go by Example: https://gobyexample.com/ (本文的很多例子摘自这个网站)
Go Tutorial: http://www.tutorialspoint.com/go/index.htm
The official Go Language specification: https://golang.org/ref/spec
Go语言编程(许式伟等编)

1.1 Hello World

首先,编写一个Hello World程序:

package main

import "fmt"

func main() {
    fmt.Printf("Hello, World\n")
}

直接执行(在临时目录中编译后运行):

$ go run hello.go
Hello, World

编译运行:

$ go build hello.go
$ ls
hello hello.go
$ ./hello
Hello, World

1.2 查看帮助文档(godoc)

使用 godoc package [name ...] 可以查看函数的帮助文档。如:

$ godoc fmt Println
func Println(a ...interface{}) (n int, err error)
    Println formats using the default formats for its operands and writes to
    standard output. Spaces are always added between operands and a newline
    is appended. It returns the number of bytes written and any write error
    encountered.

2 变量和常量

2.1 变量

可以用 var 关键字来声明变量,变量的类型信息放在变量名之后。也可以用简写形式 := 定义变量,这时不需要 var 关键字,也可以省略类型信息。

// file variables.go
package main

import "fmt"

func main() {

    var a string = "initial"   // 使用var关键字声明变量
    fmt.Println(a)

    var b, c int = 1, 2        // 可同时声明两个变量,并对它们分别赋初始值
    fmt.Println(b, c)

    var d = true               // 省略类型,通过初始值可推导类型。这里相当于 var d bool = true
    fmt.Println(d)

    var e int                  // 变量不显式初始化时,其默认值为0。所以下行语句会输出0
    fmt.Println(e)

    f := "test"                // := 是变量声明的简写形式。这里相当于 var f string = "test"
    fmt.Println(f)

    const g = "ABC"            // 用const可声明常量
    fmt.Println(g)
}

2.1.1 匿名变量(_)

Go中 _ 表示匿名变量。匿名变量常用于忽略函数中我们不关心的那些返回值。

func GetName() (firstName, lastName, nickName string) {  // 函数GetName有3个返回值
    return "May", "Chan", "Chibi Maruko"
}

_, _, nickName := GetName()             // 我们不关心函数GetName的第1个和第2个返回值

2.1.2 同时定义多个变量

() 可以同时定义多个变量,例如:

package main

import "fmt"

func main() {
    var (
      a = 1
      b = 2
      c = 3
    )
    fmt.Println(a)
    fmt.Println(b)
    fmt.Println(c)
}

类似地,用同样方法可以一次定义多个常量(把var改为const即可)。

2.2 常量

const 关键字可以声明常量。如:

const Pi float64 = 3.14159265358979323846

const (                  // 同时声明两个常量
	a int = 10
	b int = 20
)

2.2.1 字面常量(无类型)

在其他语言(如C语言)中,常量通常有特定的类型。比如-12在C语言中会认为是一个int类型的常量;如果要指定一个值为-12的long类型常量,需要写成-12l,多少有点啰嗦。

Go语言的字面常量是无类型的。 只要这个常量在相应类型的值域范围内,就可以作为该类型的常量,比如上面的常量-12,它可以赋值给int、uint、int32、int64、float32、float64、complex64、complex128等类型的变量。

2.2.2 标识符iota

标识符 iota 比较特殊,可以被认为是一个可被编译器修改的常量,在每一个const关键字出现时被重置为0。使用一个const语句声明多个常量时,每声明一个常量,iota其所代表的数字会自动增1。

const x = iota  // x == 0  (iota has been reset)
const y = iota  // y == 0  (iota has been reset)

const ( // iota is reset to 0
	c0 = iota  // c0 == 0
	c1 = iota  // c1 == 1
	c2 = iota  // c2 == 2
)

注1:如果赋值语句的表达式是一样的,那么可以省略后面的赋值表达式。如:

const (
    c0 = iota
    c1          // 相当于 c1 = iota
    c2          // 相当于 c2 = iota
)

注2:用一个const语句同时声明多个常量时,如果某个常量并没有使用iota,iota的值也会增加1。如:

const ( // iota is reset to 0
	a = 1 << iota  // a == 1      这行的iota为0
	b = 1 << iota  // b == 2      这行的iota为1
	c = 3          // c == 3      注意:iota is not used but still incremented
	d = 1 << iota  // d == 8      这行的iota为3!
)

注3:如果多个iota用在同一个ExpressionList中,iota不会变。如:

const (
	bit0, mask0 = 1 << iota, 1<<iota - 1  // bit0 == 1, mask0 == 0    这行的两个iota都为0
	bit1, mask1                           // bit1 == 2, mask1 == 1    省略了赋值表达式
	_, _                                  // skips iota == 2          省略了赋值表达式
	bit3, mask3                           // bit3 == 8, mask3 == 7    省略了赋值表达式
)

说明:C++中也有 iota ,名称iota来源于APL语言。

3 基本类型

3.1 数字类型

Table 1: Numeric types in Go
Type name Description
uint8 the set of all unsigned 8-bit integers (0 to 255)
uint16 the set of all unsigned 16-bit integers (0 to 65535)
uint32 the set of all unsigned 32-bit integers (0 to 4294967295)
uint64 the set of all unsigned 64-bit integers (0 to 18446744073709551615)
int8 the set of all signed 8-bit integers (-128 to 127)
int16 the set of all signed 16-bit integers (-32768 to 32767)
int32 the set of all signed 32-bit integers (-2147483648 to 2147483647)
int64 the set of all signed 64-bit integers (-9223372036854775808 to 9223372036854775807)
float32 the set of all IEEE-754 32-bit floating-point numbers
float64 the set of all IEEE-754 64-bit floating-point numbers
complex64 the set of all complex numbers with float32 real and imaginary parts
complex128 the set of all complex numbers with float64 real and imaginary parts
byte alias for uint8
rune alias for int32
uint either 32 or 64 bits (implementation-specific)
int same size as uint (implementation-specific)
uintptr an unsigned integer large enough to store the uninterpreted bits of a pointer value (implementation-specific)

参考:https://golang.org/ref/spec#Types

3.2 字符串

可以用双引号或者反引号来创建“字符串常量”。它们的区别在于:双引号括起来的字符串常量允许转义(比如 "\n" 表示换行),而反引号括起来的字符串常量不允许转义。如:

package main

import "fmt"

func main() {
    fmt.Println("Hello World")
    fmt.Println(`Hello World`)
    fmt.Println("Hello \nWorld")
    fmt.Println(`Hello \nWorld`)
}

上面程序将输出:

Hello World
Hello World
Hello
World
Hello \nWorld

使用内置函数 len() 可以计算字符串的长度;使用操作符 + 可以拼接两个字符串,如:

package main

import "fmt"

func main() {
  fmt.Println(len("Hello World"))    // 输出 11
  fmt.Println("Hello " + "World")    // Hello World
  fmt.Println(len("中文"))           // 输出 6(utf8编码),而不是 2
}

说明: len() 计算的是字符串所占的字节数(如上面例子中字符串“中文”的长度输出不是2),如果你希望得到Unicode字符串中的字符数,可以使用 RuneCountInString

3.3 字符

Go语言有两种字符类型,一个是byte(它是uint8的别名),代表UTF-8字符串的单个字节的值;另一个是rune(它是int32的别名),代表“单个Unicode字符”。

参考:
https://golang.org/pkg/builtin/#byte
https://golang.org/pkg/builtin/#rune

3.3.1 rune literal(单引号)

单引号表示rune literal。如:

package main

import "fmt"

func main() {
    var x rune = 'a'             // rune是int32的别名,这行也可以写为var x int32 = 'a'
    var y rune = '\n'
    var z rune = '我'
    fmt.Println(x)
    fmt.Println(y)
    fmt.Println(z)
}

输出:

97
10
25105

参考:https://golang.org/ref/spec#Rune_literals

3.4 布尔类型

go中支持Booleans类型。如:

package main

import "fmt"

func main() {
    var a bool = true
    fmt.Println(a)             // true
    fmt.Println(true)          // true
    fmt.Println(false)         // false
    fmt.Println(true || true)  // true
    fmt.Println(true || false) // true
    fmt.Println(true && true)  // true
    fmt.Println(true && false) // false
    fmt.Println(!true)         // false
    fmt.Println(!false)        // true
}

4 基本控制结构

4.1 If else语句

If else语句的实例如下。注意条件表达式不要用小括号包围,但语句一定要有大括号。

package main

import "fmt"

func main() {

    // Here's a basic example.
    if 7%2 == 0 {
        fmt.Println("7 is even")
    } else {
        fmt.Println("7 is odd")
    }

    // You can have an `if` statement without an else.
    if 8%4 == 0 {
        fmt.Println("8 is divisible by 4")
    }

    // A statement can precede conditionals; any variables
    // declared in this statement are available in all
    // branches.
    if num := 9; num < 0 {
        fmt.Println(num, "is negative")
    } else if num < 10 {
        fmt.Println(num, "has 1 digit")
    } else {
        fmt.Println(num, "has multiple digits")
    }
}

4.2 For语句

有3种形式的for语句。Go语言中没有while语句。

package main

import "fmt"

func main() {

    // The most basic type, with a single condition.
    i := 1
    for i <= 3 {
        fmt.Println(i)
        i = i + 1
    }

    // A classic initial/condition/after `for` loop.
    for j := 7; j <= 9; j++ {
        fmt.Println(j)
    }

    // `for` without a condition will loop repeatedly
    // until you `break` out of the loop or `return` from
    // the enclosing function.
    for {
        fmt.Println("just once")
        break
    }
}

4.3 Switch语句


Go中有两种Switch语言:“expression switches”和“type switches”。这里仅介绍“expression switches”,关于“type switches”可参考节 12.3

Switch语句的一个分支结束时,不需要break语句(这点和C语言中Switch语句是不同的)。Go中Switch语句的基本用法如下:

package main

import "fmt"
import "time"

func main() {

    // Here's a basic `switch`.
    i := 2
    fmt.Print("write ", i, " as ")
    switch i {
    case 1:
        fmt.Println("one")                     // 和C语言不同,一个分支结束时,不需要break语句
    case 2:
        fmt.Println("two")
    case 3:
        fmt.Println("three")
    }

    // You can use commas to separate multiple expressions
    // in the same `case` statement. We use the optional
    // `default` case in this example as well.
    switch time.Now().Weekday() {
    case time.Saturday, time.Sunday:           // 逗号可以分隔多个case语句
        fmt.Println("it's the weekend")
    default:
        fmt.Println("it's a weekday")
    }
}

Switch语句还有一种形式,即省略switch关键字后面的表达式,这时和多个if…else…语句的逻辑相同。如:

package main

import "fmt"
import "time"

func main() {

    // `switch` without an expression is an alternate way
    // to express if/else logic. Here we also show how the
    // `case` expressions can be non-constants.
    t := time.Now()
    switch {                                      // switch后面可以不接表达式
    case t.Hour() < 12:
        fmt.Println("it's before noon")
    default:
        fmt.Println("it's after noon")
    }
}

4.3.1 fallthrough

前面介绍过,Go中Switch语句的一个分支结束时,不需要break语句。如果我们想要匹配某个分支后强制执行下一个的case代码,这时可以使用 fallthrough 关键字。如:

package main

import "fmt"

func main() {

    i := 2

    switch i {
    case 0:
        fmt.Println("0")
    case 1:
        fmt.Println("1")
    case 2:
        fmt.Println("2")
        fallthrough                    // 如果进到这个分支,会强制执行下一个的case代码
    case 3:
        fmt.Println("3")
    case 4, 5, 6:
        fmt.Println("4, 5, 6")
    default:
        fmt.Println("Default")
    }
}

上面程序会输出:

2
3

显然,最后一个分支是不能使用 fallthrough 关键字的,因为最后一个分支没有下一个分支了。

5 Arrays, Slices, Maps

5.1 Arrays(var x [n]T)

To declare an array in Go, a programmer specifies the type of the elements and the number of elements required by an array as follows:

var variable_name [SIZE] variable_type
package main

import "fmt"

func main() {

    // Here we create an array `a` that will hold exactly
    // 5 `int`s. The type of elements and length are both
    // part of the array's type. By default an array is
    // zero-valued, which for `int`s means `0`s.
    var a [5]int
    fmt.Println("emp:", a)

    // We can set a value at an index using the
    // `array[index] = value` syntax, and get a value with
    // `array[index]`.
    a[4] = 100
    fmt.Println("set:", a)
    fmt.Println("get:", a[4])

    // The builtin `len` returns the length of an array.
    fmt.Println("len:", len(a))

    // Use this syntax to declare and initialize an array
    // in one line.
    b := [...]int{1, 2, 3, 4, 5}                          // 能自动计算数组的大小。相当于 b := [5]int{1, 2, 3, 4, 5}
    fmt.Println("dcl:", b)

    // Array types are one-dimensional, but you can
    // compose types to build multi-dimensional data
    // structures.
    var twoD [2][3]int                                    // 二维数组
    for i := 0; i < 2; i++ {
        for j := 0; j < 3; j++ {
            twoD[i][j] = i + j
        }
    }
    fmt.Println("2d: ", twoD)
}

执行上面程序,会输出:

emp: [0 0 0 0 0]
set: [0 0 0 0 100]
get: 100
len: 5
dcl: [1 2 3 4 5]
2d:  [[0 1 2] [1 2 3]]

5.1.1 数组有边界检查

Go语言中,会自动对数组进行边界检查,超过边界的访问会报错。

package main

import "fmt"

func main() {
  var a [2]int
  for i := 0 ; i<15 ; i++ {
    fmt.Printf("Element: %d %d\n", i, a[i])
  }
}

运行上面程序时,会提示错误:

Element: 0 0
Element: 1 0
panic: runtime error: index out of range

5.2 Slices

Arrays是固定大小的,不太方便。Go中提供了一个新的类型——Slices,它是可变长度的。在Go中,Slices比Arrays更常用。

有两种方法定义一个Slice:

var numbers []int                   /* a slice of unspecified size */
var numbers = make([]int, 3, 5)     /* a slice of length 3 and capacity 5 */

5.2.1 内置的len()和cap()函数

Slice actually uses array as an underlying structure. len() function returns the elements presents in the slice where cap() function returns the capacity of slice as how many elements it can be accomodate.

Slice简单实例:

package main

import "fmt"

func main() {
   var numbers = make([]int, 3, 5)                             // 指定length为3,capacity为5
   printSlice(numbers)
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)   // 输出len=3 cap=5 slice=[0 0 0]
}

5.2.2 内置的append()和copy()函数

Slice allows increasing the capacity of a slice using append() function. Using copy() function, contents of a source slice are copied to destination slice. Following is the example:

package main

import "fmt"

func main() {
   var numbers []int
   printSlice(numbers)

   /* append allows nil slice */
   numbers = append(numbers, 0)
   printSlice(numbers)

   /* add one element to slice */
   numbers = append(numbers, 1)
   printSlice(numbers)

   /* add more than one element at a time */
   numbers = append(numbers, 2,3,4)
   printSlice(numbers)

   /* create a slice numbers1 with double the capacity of earlier slice */
   numbers1 := make([]int, len(numbers), (cap(numbers))*2)

   /* copy content of numbers to numbers1 */
   copy(numbers1, numbers)
   printSlice(numbers1)
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

运行上面程序,可能得到下面结果:

len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]

注意:上面输出中cap的值在不同的Go实现中可能不一样。

5.3 range关键字

The range keyword is used in for loop to iterate over items of an array, slice, channel or map.

Table 2: range返回1个或2个值
Range expression 1st Value 2nd Value(Optional)
Array or slice a [n]E index i int a[i] E
String s string type index i int rune int
map m map[K]V key k K value m[k] V
channel c chan E element e E none

range实例:

package main

import "fmt"

func main() {

    // Here we use `range` to sum the numbers in a slice.
    // Arrays work like this too.
    nums := []int{2, 3, 4}
    sum := 0
    for _, num := range nums {     // range作用于slice时返回2个值,这里用`_`表示不关心第1个值
        sum += num
    }
    fmt.Println("sum:", sum)

    for i, num := range nums {
        if num == 3 {
            fmt.Println("index:", i)
        }
    }
}

5.4 Maps(var x map[T]T)

Maps就是哈希表,在使用前需要用内置函数 make() 初始化。如:

var myMap map[string] int          // 声明一个Map
myMap = make(map[string] int)      // 初始化Map
myMap["key1"] = 100                // 往Map中插入元素 key1:100
myMap["key2"] = 200                // 往Map中插入元素 key2:200

也可以在定义时就给map赋值,这种情况下不用使用make()进行初始化。 如:

elements := map[string]string{
  "H":  "Hydrogen",
  "He": "Helium",
  "Li": "Lithium",
  "Be": "Beryllium",
  "B":  "Boron",
  "C":  "Carbon",
  "N":  "Nitrogen",
  "O":  "Oxygen",
  "F":  "Fluorine",
  "Ne": "Neon",
}

5.4.1 删除元素:内置的delete()函数

使用内置函数 delete() 可以删除Map中的元素,如果待删除的元素不存在 delete() 也不会报错。如:

delete(myMap, "key1")       // 删除myMap中key为“key1”的元素,不存在也不报错

5.4.2 查找key对应的value,判断key是否存在

要从map中查找一个特定的键,可以通过下面的代码来实现:

value, ok := myMap["key1"]
if ok {  // 找到了“key1”
// 处理找到的value
} else { // 没有找到“key1”
// 其他处理
}

5.4.3 Map完整实例

package main

import "fmt"

func main() {
    var countryCapitalMap map[string]string            // string -> string

    /* create a map */
    countryCapitalMap = make(map[string]string)

    /* insert key-value pairs in the map */
    countryCapitalMap["France"] = "Paris"
    countryCapitalMap["Italy"] = "Rome"
    countryCapitalMap["Japan"] = "Tokyo"
    countryCapitalMap["India"] = "New Delhi"

    fmt.Println(countryCapitalMap);

    /* delete an entry */
    delete(countryCapitalMap, "India");                       // delete函数可以删除map中元素

    fmt.Println(countryCapitalMap);

    /* print map */
    for country, capital := range countryCapitalMap {
        fmt.Println("Capital of", country, "is", capital)
    }

    /* test if entry is present in the map or not */
    capital, ok := countryCapitalMap["United States"]
    /* if ok is true, entry is present otherwise entry is absent */
    if ok {
        fmt.Println("Capital of United States is", capital)
    } else {
        fmt.Println("Capital of United States is not present")
    }
}

上面程序的输出如下:

map[France:Paris Italy:Rome Japan:Tokyo India:New Delhi]
map[France:Paris Italy:Rome Japan:Tokyo]
Capital of France is Paris
Capital of Italy is Rome
Capital of Japan is Tokyo
Capital of United States is not present

5.5 Slices, Maps and Channels是引用类型

Slices, Maps and Channels是引用类型。数组则不是引用类型。
These are reference types, meaning that you always access them via a reference. For example, if you assign one map-typed variable to another, then you will have two variables referring to the same map.

5.5.1 数组不是引用类型

在C语言中,数组比较特别:通过函数传递一个数组时传递的是数组首元素的地址(像“引用类型”),而在结构体中定义数组变量对它赋值时数组会被完整地复制(像“值类型”)。

在Go语言中,数组是彻底的值类型。如果函数的参数是数组类型,则调用函数时会复制整个数组。如:

package main

import "fmt"

func main() {
    var a [3]string;
    a[0] = "test1";
    a[1] = "test2";
    a[2] = "test3";

    change1(a);
    print(a[:]);     // 相当于 print(a[0:3]);

    change2(a[:]);
    print(a[:]);
}

func change1(array [3]string) {    // 它的参数是数组,调用函数时会复制整个数组!函数内对array的修改不会影响函数外的数组
    array[2] = "new1";
}

func change2(array []string) {     // 它的参数是Slices。函数内对array的修改会影响函数外的数组
    array[2] = "new2";
}

func print(array []string) {       // 它的参数是Slices
    for i := 0; i < len(array); i++ {
        fmt.Println(array[i])
    }
}

运行上面程序,会输出:

test1
test2
test3
test1
test2
new2

从上面的输出可知,change1(参数类型为数组)并不会修改数组a(因为它只是修改的复制数组的副本),而change2修改了数组a。

6 函数

关键字 func 可以用来定义一个函数。其形式如下:

func function_name( [parameter list] ) [return_types] {
   // body of the function
}

函数实例:

package main
import "fmt"

func max(a int, b int) int {     // 说明:对参数类型的说明在参数名之后。
    if a > b {
        return a
    }
    return b
}

func main() {
    fmt.Println(max(2, 3))
}

如果参数列表中若干个相邻的参数类型的相同(比如上面例子中max函数的参数a和b),则可以在参数列表中省略前面变量的类型声明,如前面例子的max函数还可写为:

func max(a, b int) int {        // 参数列表中省略了前面变量的类型声明
    if a > b {
        return a
    }
    return b
}

注: Go语言不支持函数重载(Overload)。 如果有两个函数的参数不同,但函数名相同,编译器会报错。

6.1 函数可返回多个值

Go函数可以返回多个值,用逗号分开即可。如:

package main

import "fmt"

func swap(x, y string) (string, string) {   // 同时返回两个值
   return y, x
}

func main() {
   a, b := swap("abc", "xyz")
   fmt.Println(a, b)                        // 输出 xyz abc
}

6.2 参数传递方式为“值传递”

package main

import "fmt"

func fun_foo(a int) (int) {
    a = 5
    return a
}

func main() {
    var x = 1
    fun_foo(x)
    fmt.Println(x)              // 参数为值传递,会输出1
}

Go语言所有传参都是“值传递”,函数里得到的是一个拷贝。当参数是非引用类型(int、string、struct等)时,在函数中无法修改原数据;当参数是引用类型(指针、map、slice、chan)时,可以修改原内容数据。

6.3 变长参数函数(…)

Go支持变长参数函数。实例如下:

package main
import "fmt"

func add(args ...int) int {    // ... 表示变长参数
  total := 0
  for _, v := range args {
    total += v
  }
  return total
}

func main() {
  fmt.Println(add(1,2))      // 输出 3
  fmt.Println(add(1,2,3))    // 输出 6
}

6.3.1 传递slice到变长参数

有时我们想要把slice变量(如a)传递给接收变长参数的函数,这时传递 a... 即可,如:

package main

import "fmt"

func echo(strings ...string) {   // echo 接收变长参数
    for _, s := range strings {
        fmt.Println(s)
    }
}

func main() {
    a := []string{"str1", "str2", "str3"}

	// echo(a)                    // 这行会报错,echo接收变长参数,而不是slice
	echo("str1", "str2", "str3")
	echo(a...)                    // 输出和上行相同
}

参考:https://golang.org/ref/spec#Passing_arguments_to_..._parameters

6.4 Create Functions on the Fly

Go programming language provides flexibility to create functions on the fly and use them as values.

package main

import (
    "fmt"
    "math"
)

func main(){
    /* declare a function variable */
    getSquareRoot := func(x float64) float64 {
        return math.Sqrt(x)
    }

    /* use the function */
    fmt.Println(getSquareRoot(9))

    i := 100
    func() {                 /* 创建函数后,马上调用它 */
        fmt.Println(i)
    } ()
}

参考:http://www.tutorialspoint.com/go/go_function_as_values.htm

6.5 Function Closure(函数闭包)

Go programming language supports anonymous functions which can acts as function closures.

package main

import "fmt"

func getSequence() func() int {           // 函数getSequence返回另一个函数
   i:=0
   return func() int {
      i+=1
	  return i
   }
}

func main(){
   /* nextNumber is now a function with i as 0 */
   nextNumber := getSequence()

   /* invoke nextNumber to increase i by 1 and return the same */
   fmt.Println(nextNumber())
   fmt.Println(nextNumber())
   fmt.Println(nextNumber())

   /* create a new sequence and see the result, i is 0 again */
   nextNumber1 := getSequence()
   fmt.Println(nextNumber1())
   fmt.Println(nextNumber1())
}

When the above code is compiled and executed, it produces the following result:

1
2
3
1
2

参考:http://www.tutorialspoint.com/go/go_function_closures.htm

6.5.1 再看闭包

什么是闭包?如果函数中存在“非局部且非全局的变量”,则函数和“非局部且非全局的变量”一起构成“闭包”。

package main
import "fmt"

func main() {
  x := 0                      // 本行和下面4行构造闭包
  increment := func() int {   // 变量 x 和函数 increment 构成闭包
    x++                       // 变量 x 不是函数 func 的局部变量,也不是全局变量
    return x
  }
  fmt.Println(increment())
  fmt.Println(increment())
}

使用闭包的常见方法是写一个函数,它返回另外一个函数。如:

package main
import "fmt"

func makeEvenGenerator() func() uint {
  i := uint(0)
  return func() (ret uint) {
    ret = i
    i += 2
    return
  }
}
func main() {
  nextEven := makeEvenGenerator()
  fmt.Println(nextEven()) // 0
  fmt.Println(nextEven()) // 2
  fmt.Println(nextEven()) // 4
}

参考:https://www.golang-book.com/books/intro/7#section4

6.6 defer语句

Go语言中有个特别的defer语句,在当前函数退出前才执行defer关联的函数。比如:

package main

import "fmt"

func first() {
  fmt.Println("1st")
}
func second() {
  fmt.Println("2nd")
}
func main() {
  defer second()   // 当前函数main退出前,才执行second()函数
  first()
}

defer常常用于资料的释放。如:

f, _ := os.Open(filename)
defer f.Close()

This has 3 advantages:
(1) it keeps our Close call near our Open call so it's easier to understand,
(2) if our function had multiple return statements (perhaps one in an if and one in an else) Close will happen before both of them.
(3) deferred functions are run even if a run-time panic occurs.

注:如果在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照“逆序”执行,最后该函数返回。

7 指针

和C类似,Go支持指针。指针的 *& 操作符和C类似。

package main

import "fmt"

func main() {
   var a int = 10
   var ptr *int = &a

   fmt.Printf("Address of a variable: %x\n", ptr)
   fmt.Printf("%d\n", a)                              // 输出10
   fmt.Printf("%d\n", *ptr)                           // 输出10
}

7.1 Array of pointers

package main
import "fmt"
const MAX int = 3

func main() {
   a := []int{10,100,200}
   var i int
   var ptr [MAX]*int;                    // 声明ptr为指针数组

   for  i = 0; i < MAX; i++ {
      ptr[i] = &a[i]                     /* assign the address of integer. */
   }

   for  i = 0; i < MAX; i++ {
      fmt.Printf("Value of a[%d] = %d\n", i, *ptr[i])
   }
}

7.2 内置的new()函数

Another way to get a pointer is to use the built-in new function. new takes a type as an argument, allocates enough memory to fit a value of that type and returns a pointer to it.

package main
import "fmt"

func one(xPtr *int) {
  *xPtr = 1
}
func main() {
  xPtr := new(int)
  one(xPtr)
  fmt.Println(*xPtr)   // 输出 1
}

8 结构体

Go中声明结构体的用法如下:

type struct_variable_type struct {
   member definition;
   member definition;
   ...
   member definition;
}

简单测试例子如下:

package main

import "fmt"

type Books struct {        // 定义结构体
   title string
   author string
   subject string
   book_id int
}

func main() {
   var Book1 Books        /* Declare Book1 of type Book */
   var Book2 Books        /* Declare Book2 of type Book */

   /* book 1 specification */
   Book1.title = "Go Programming"
   Book1.author = "Mahesh Kumar"
   Book1.subject = "Go Programming Tutorial"
   Book1.book_id = 6495407

   /* book 2 specification */
   Book2.title = "Telecom Billing"
   Book2.author = "Zara Ali"
   Book2.subject = "Telecom Billing Tutorial"
   Book2.book_id = 6495700

   printBook1(Book1)

   printBook2(&Book2)
}

func printBook1( book Books ) {
   fmt.Printf( "Book title : %s\n", book.title);          // 通过结构体访问结构体成员用点号(.)
   fmt.Printf( "Book author : %s\n", book.author);
   fmt.Printf( "Book subject : %s\n", book.subject);
   fmt.Printf( "Book book_id : %d\n", book.book_id);
}

func printBook2( book *Books ) {
   fmt.Printf( "Book title : %s\n", book.title);          // 通过指针访问结构体成员也用点号(.),这和C语言不同。
   fmt.Printf( "Book author : %s\n", book.author);
   fmt.Printf( "Book subject : %s\n", book.subject);
   fmt.Printf( "Book book_id : %d\n", book.book_id);
}

8.1 Struct Literals

创建结构体时可以同时为结构体的域指定对应的值。如:

package main

import "fmt"

type Vertex struct {
    X, Y int
}

var (
    v1 = Vertex{1, 2}  // has type Vertex
    v2 = Vertex{X: 1}  // 可以用key:value形式初始化!这里仅指定了X为1,没有指定的域默认为0
    v3 = Vertex{}      // X:0 and Y:0
    p  = &Vertex{1, 2} // has type *Vertex
)

func main() {
    fmt.Println(v1)
    fmt.Println(v2)
    fmt.Println(v3)
    fmt.Println(p)
}

上面程序的输出:

{1 2}
{1 0}
{0 0}
&{1 2}

9 错误处理(panic & recover)

Go中没有像C++/Java中的try…catch语句。Go中可以使用函数 panic 和 recover 来实现类似异常处理的功能。

Built-in panic function cause a run time error. Built-in functioin recover stops the panic and returns the value that was passed to the call to panic.

请看下面例子:

package main
import "fmt"

func main() {
  panic("PANIC, MY GOD")
  str := recover()        # 由于panic会异常main函数马上停止执行,所以recover()并不会执行
  fmt.Println(str)
}

上面程序执行时,panic还是会生效,recover并没阻止panic(因为recover根本没有执行)。如:

$ go run main.go
panic: PANIC, MY GOD

goroutine 1 [running]:
main.main()
	/Users/cig01/test/main.go:5 +0x65
exit status 2

可以把前面程序修改为:

package main
import "fmt"

func main() {
  defer func() {
    str := recover()
    fmt.Println(str)
  }()
  panic("PANIC, MY GOD")
}

这样,recover可以成功地阻止panic。如:

$ go run main.go
PANIC, MY GOD

10 面向对象编程

对于面向对象编程的支持Go语言设计得非常简洁。简洁之处在于,Go语言放弃了传统面向对象编程中的诸多概念,比如继承、虚函数、构造函数和析构函数、隐藏的this指针等。

本节内容主要参考:《Go语言编程(许式伟等编)》

10.1 类型系统

类型系统是指一个语言的类型体系结构。一个典型的类型系统通常包含如下基本内容:

  • 基础类型,如byte、int、bool、float等;
  • 复合类型,如数组、结构体、指针等;
  • 可以指向任意对象的类型(Any类型);
  • 值语义和引用语义;
  • 面向对象,即所有具备面向对象特征(比如成员方法)的类型;
  • 接口。

类型系统描述的是这些内容在一个语言中如何被关联。

10.1.1 声明新类型,给类型添加方法

Go中用 type 可以基于某个类型声明一个新类型(Go中的type相当于Java等语言的class)。

例如, type T1 string 表示声明T1是 string 类型。

下面例子声明了一个新类型Integer,且为类型Integer增加了一个新方法Less()。

type Integer int

func (a Integer) Less(b Integer) bool {   // a Integer称为receiver,其中Integer是receiver type
    return a < b
}

这样,我们在使用Integer时,可以像一个普通的类一样使用。如:

// 面向对象的方式
package main
import "fmt"

/* 这里省略了Integer定义等内容 */

func main() {
    var a Integer = 1
    if a.Less(2) {                      // 在a上调用Less
        fmt.Println(a, "Less 2")        // 输出 1 Less 2
    }
}

如果不使用面向对象的方式,而是使用面向过程的方式,则相应的实现细节为:

// 面向过程的方式
package main

import "fmt"

type Integer int

func Integer_Less(a Integer, b Integer) bool {
    return a < b
}
func main() {
    var a Integer = 1
    if Integer_Less(a, 2) {             // 调用形式和面向对象的调用形式不一样
        fmt.Println(a, "Less 2")
    }
}

对比两种形式,可以看出:面向对象只是换了一种语法形式来表达。

C++/Java/C#等语言都包含有一个隐藏的this指针(方法施加的目标)。如用Java实现和前面功能类似的代码:

class Integer {
  private int val;
  public boolean Less(Integer b) {  // 可以理解为Less方法有两个参数,但隐藏了第一个参数this
    return this.val< b.val;         // 直接使用this即可,它代表了方法施加的目标(也就是“对象”)
  }
}

如果将上面代码翻译成C代码,会更清晰:

struct Integer {
    int val;
};
bool Integer_Less(Integer* this, Integer* b) {
     return this->val < b->val;
}

在Go语言中没有隐藏的this指针 ,这句话的含义是:

  • 方法施加的目标(也就是“对象”)显式传递,没有被隐藏起来;
  • 方法施加的目标(也就是“对象”)不需要非得是指针,也不用非得叫this。

只有在你需要修改对象的时候,才必须用指针。它不是Go语言的约束,而是一种自然约束。举个例子:

func (a *Integer) Add(b Integer) {
    *a += b
}

这里为Integer类型增加了Add()方法。由于Add()方法需要修改对象的值,所以需要用指针引用。调用如下:

func main() {
    var a Integer = 1
    a.Add(2)
    fmt.Println("a =", a)     // 输出 a = 3
}

如果你实现成员方法时传入的不是指针而是值(即传入Integer,而非*Integer),如下所示:

func (a Integer) Add(b Integer) {
    a += b
}

那么运行程序得到的结果是“a = 1”,也就是维持原来的值。Go语言和C语言一样,类型都是基于值传递的。要想修改变量的值,只能传递指针。

10.2 再看结构体

Go语言的结构体(struct)和其他语言的类(class)有同等的地位,但Go语言放弃了包括继承在内的大量面向对象特性,只保留了组合(composition)这个最基础的特性。

10.2.1 匿名字段(Anonymous fields)

下面例子中定义了两个类型Person和Employee:

package main
import "fmt"

type Person struct {
    name string
    age  int
    addr string
}

type Employee struct {
    person Person
    salary int
}

func main() {
    var em1 Employee;
    em1.person = Person{"cig01", 10, "Guangming Road"};
    em1.salary = 10000;
    fmt.Println(em1);              // 输出 {{cig01 10 Guangming Road} 10000}
    fmt.Println(em1.person.name);  // 输出 cig01
    fmt.Println(em1.salary);       // 输出 10000
}

从上面例子中可以看到,使用 em1.person.name 可以访问Employee的名字。有没有办法可以使用 em1.name 直接访问Employee的名字?有,使用匿名字段(Anonymous fields)即可。

在Go中,结构体中的匿名字段(Anonymous fields)是指只声明字段的类型,而省略其名字的字段。 匿名字段的实例如下:

package main
import "fmt"

type Person struct {
    name string
    age  int
    addr string
}

type Employee struct {
    Person              // 这是匿名字段(Anonymous fields),只声明字段的类型,而省略其名字
    salary int
}

func main() {
    var em1 Employee = Employee{Person{"cig01", 10, "Guangming Road"}, 10000};
    fmt.Println(em1);              // 输出 {{cig01 10 Guangming Road} 10000}
    fmt.Println(em1.name);         // 输出 cig01
    fmt.Println(em1.salary);       // 输出 10000
}
10.2.1.1 名字冲突

如果一个类型包含多个匿名字段,这些匿名字段相应类型中有相同名字的域,则会出现“可能的”名字冲突问题。之所以说“可能的”,是因为编译器仅当用户代码中引用冲突的field时才会报错,如果用户代码中不引用冲突的field,则不会报错。

匿名字段导致名字冲突的实例如下:

package main
import "fmt"

type X struct {
    Name string    // 存在名为Name的域
    xxx int
}
type Y struct {
    Name string    // 这里也存在名为Name的域
    yyy int
}

type Z struct {
    X            // 匿名字段
    Y            // 匿名字段
}

func main() {
    var z Z;
    fmt.Println(z.xxx);
    fmt.Println(z.yyy);
    fmt.Println(z.Name);      // 编译器会报错!提示ambiguous selector z.Name
}

上面实例中,如果把“fmt.Println(z.Name);”这一行注释掉,则编译器会忽略掉冲突问题(反正用户代码中没有引用冲突的field),顺利通过编译。

10.2.1.2 名字隐藏(类似C++中的overriding)

考虑下面的例子。组合的类型(Employee)和被组合的类型(Persion)都包含一个名为addr的成员,会不会有问题呢?答案是不会有问题。默认被组合的类型中的addr会被隐藏(通过em1.addr访问不到它),不过通过Person.addr的方式可以访问到会被隐藏的addr。

名字隐藏的例子:

package main
import "fmt"

type Person struct {
    name string
    age  int
    addr string        // 这里有addr
}

type Employee struct {
    Person             // 匿名字段
    salary int
    addr string        // 这里也有addr。会覆盖(overriding)匿名字段Person中的addr
}

func main() {
    var em1 Employee = Employee{Person{"cig01", 10, "Guangming Road"}, 5000, "Chaoyang Road"}
    fmt.Println(em1);                 // 输出 {{cig01 10 Guangming Road} 10000}
    fmt.Println(em1.addr);            // 输出 Chaoyang Road
    fmt.Println(em1.Person.addr);     // 输出 Guangming Road
}

10.3 可见性

Go语言对关键字的增加非常吝啬,其中没有private、protected、public这样的关键字。 要使某个符号对其他包(package)可见(即可以访问),需要将该符号定义为以大写字母开头 ,如:

type Rect struct {
    X, Y float64
    Width, Height float64
}

这样,Rect类型的成员变量就全部被导出了,可以被所有其他引用了Rect所在包的代码访问到。
成员方法的可访问性遵循同样的规则,例如:

func (r *Rect) area() float64 {
    return r.Width * r.Height
}

这样,Rect的area()方法只能在该类型所在的包内使用。
需要注意的一点是, Go语言中符号的可访问性是包一级的而不是类型一级的。 在上面的例子中,尽管area()是Rect的内部方法,但同一个包中的其他类型也都可以访问到它。这样的可访问性控制很粗旷,但是非常实用。如果Go语言符号的可访问性是类型一级的,少不了还要加上friend这样的关键字,以表示两个类是朋友关系,可以访问彼此的私有成员。

10.4 用接口(Interface)实现多态

Go语言的主要设计者之一罗布·派克(Rob Pike)曾经说过,如果只能选择一个Go语言的特性移植到其他语言中,他会选择接口。接口在Go语言有着至关重要的地位。

Go中用接口(Interface)实现多态。 接口是“方法签名”的集合。

假设定义了方法method_name1/method_name2/method_name3等等:

/* define a struct */
type struct_name struct {
   /* variables */
}

/* implement interface methods */
func (struct_name_variable struct_name) method_name1() [return_type] {
   /* method implementation */
}

/* other methods definition: method_name2, method_name3  */

func (struct_name_variable struct_name) method_namen() [return_type] {
   /* method implementation */
}

使用下面语法可以把上面的这些“方法签名”定义为一个接口。如果一个类(type)实现了接口要求的所有函数,我们就说这个类(type)实现了该接口。

/* define an interface */
type interface_name interface {
   method_name1 [return_type]
   method_name2 [return_type]
   method_name3 [return_type]
   ...
   method_namen [return_type]
}

10.4.1 实例:接口实现多态

下面通过一个例子来演示接口是如何实现多态的。

// _Interfaces_ are named collections of method signatures.

package main

import "fmt"
import "math"

/* 定义接口,其名为geometry,它包含两个方法 */
type geometry interface {
    area() float64                // 计算面积
    perimeter() float64           // 计算周长
}

/* 定义长方形和圆形 */
type rect struct {
    width, height float64
}
type circle struct {
    radius float64
}

/* 实现长方形的接口函数 */
func (r rect) area() float64 {
    return r.width * r.height
}
func (r rect) perimeter() float64 {
    return 2*r.width + 2*r.height
}

/* 实现圆形的接口函数 */
func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

// If a variable has an interface type, then we can call
// methods that are in the named interface. Here's a
// generic `measure` function taking advantage of this
// to work on any `geometry`.
func measure(g geometry) {                // 函数measure的参数是接口geometry。如果对象实现了接口geometry,则对象可以作为它的参数。
    fmt.Println(g)
    fmt.Println(g.area())                 // g是rect时,会调用rect相关的area方法;g是circle时,会调用circle相关的area方法。这实现了多态!
    fmt.Println(g.perimeter())
}

func main() {
    r := rect{width: 3, height: 4}
    c := circle{radius: 5}

    measure(r)   // 输出长方形的面积和周长
    measure(c)   // 输出圆形的面积和周长
}

10.4.2 非侵入式接口

Go语言的接口和其他语言(C++、Java、C#等)中的接口有明显的不同。
在Go语言出现之前,接口主要作为不同组件之间的契约存在。对契约的实现是强制的,你必须声明你的确实现了该接口。为了实现一个接口,你需要从该接口继承:

interface IFoo {
    void Bar();
}
class Foo implements IFoo {  // Java文法
    // ...
}
class Foo : public IFoo {    // C++文法
    // ...
}

IFoo* foo = new Foo;

即使另外有一个接口IFoo2实现了与IFoo完全一样的接口方法甚至名字也叫IFoo只不过位于不同的名字空间下,编译器也会认为上面的类Foo只实现了IFoo而没有实现IFoo2接口。 这类接口我们称为侵入式接口。“侵入式”的主要表现在于实现类需要明确声明自己实现了某个接口。

在Go语言中,一个类(type)只需要实现了接口要求的所有函数,我们就说这个类(type)实现了该接口,而不用显式地声明某个类(type)实现了某某接口。Go中的接口可称为非侵入式接口。

10.4.3 接口赋值

10.4.3.1 对象实例赋值给接口

在Go语言中,一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口。如:

type File struct {
    // ...
}
func (f *File) Read(buf []byte) (n int, err error)
func (f *File) Write(buf []byte) (n int, err error)
func (f *File) Seek(off int64, whence int) (pos int64, err error)
func (f *File) Close() error

假设我们有下面接口:

type IFile interface {
    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
}

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

显然,File类实现了接口IFile和IReader,可以把File类实例赋值给接口IFile和IReader:

var file1 IFile = new(File)
var file2 IReader = new(File)

下面考虑一个复杂些的例子:

type Integer int
func (a Integer) Less(b Integer) bool {
    return a < b
}
func (a *Integer) Add(b Integer) {
    *a += b
}

type LessAdder interface {
    Less(b Integer) bool
    Add(b Integer)
}

var a Integer = 1
var b1 LessAdder = &a   // (1)
var b2 LessAdder = a    // (2)

上面例子中对象实例赋值给一个接口时,应该采用语句(1),还是语句(2)呢?答案是语句(1)。原因在于,Go语言可以根据下面的函数:

func (a Integer) Less(b Integer) bool

自动生成一个新的Less()方法:

// 自动生成的方法
func (a *Integer) Less(b Integer) bool {
    return (*a).Less(b)
}

这样,类型*Integer就既存在Less()方法,也存在Add()方法,满足LessAdder接口,所以可以使用语句(1)赋值。而从另一方面来说,根据

func (a *Integer) Add(b Integer)

这个方法无法自动生成以下这个方法:

func (a Integer) Add(b Integer) {
    (&a).Add(b)
}

因为(&a).Add()改变的只是函数参数a,对外部实际要操作的对象并无影响,这不符合用户的预期。所以,Go语言不会自动为其生成该函数。因此,类型Integer只存在Less()方法, 缺少Add()方法,不满足LessAdder接口,故此上面的语句(2)不能赋值。

为了进一步证明以上的推理,我们把LessAdder中的Add()方法注释掉,即为如下:

type LessAdder interface {
    Less(b Integer) bool
    //Add(b Integer)
}

可以验证,此时语句(1)和语句(2)都是合法的。

10.4.3.2 将一个接口赋值给另一个接口

接口赋值并不要求两个接口必须等价。如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A。

10.4.4 接口组合

Go语言支持接口组合。Go语言包中有io.Reader接口和io.Writer接口,同样来自于io包的另一个接口io.ReadWriter,它是io.Reader接口和io.Writer接口的组合:

// ReadWriter接口将基本的Read和Write方法组合起来
type ReadWriter interface {
    Reader
    Writer
}

它完全等同于如下写法:

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

10.4.5 空接口interface{}

当函数可以接受任意的对象实例时,我们会将其声明为空接口 interface{} ,最典型的例子是标准库fmt中PrintXXX系列的函数 ,例如:

func Printf(fmt string, args ...interface{})
func Println(args ...interface{})
......

可以认为,Go中的空接口 interface{} 和Java中的java.lang.Object类似。

11 并发编程

Go语言引入了goroutine概念,它使得并发编程变得非常简单。通过使用goroutine而不是直接用操作系统的并发机制,以及使用消息传递来共享内存而不是使用共享内存来通信,Go语言让并发编程变得更加轻盈和安全。

goroutine是一种比线程更加轻盈、更省资源的协程。Go语言通过系统的线程来多路派遣这些goroutine的执行。当一个协程阻塞的时候,调度器就会自动把其他协程安排到另外的线程中去执行,从而实现了程序无等待并行化运行。而且调度的开销非常小,一颗CPU调度的规模不下于每秒百万次,这使得我们能够创建大量的goroutine,从而可以很轻松地编写高并发程序,达到我们想要的目的。

本节内容主要参考:《Go语言编程(许式伟等编)》第1.2.7节、第4章

11.1 并发基础

并发主要有下面几种实现模型:

多进程
多进程是在操作系统层面进行并发的基本模式。同时也是开销最大的模式。
多线程
多线程在大部分操作系统上都属于系统层面的并发模式,也是我们使用最多的最有效的一种模式。它比多进程的开销小很多,但是其开销依旧比较大,且在高并发模式下,效率会有影响。
基于回调的非阻塞/异步I/O
这种架构的诞生实际上来源于多线程模式的危机。在很多高并发服务器开发实践中,使用多线程模式会很快耗尽服务器的内存和CPU资源。而这种模式通过事件驱动的方式使用异步I/O,使服务器持续运转,且尽可能地少用线程,降低开销,它目前在Node.js中得到了很好的实践。但是使用这种模式,编程比多线程要复杂,因为它把流程做了分割,对于问题本身的反应不够自然。
协程(轻量级线程)
协程(Coroutine)本质上是一种用户态线程,不需要操作系统来进行抢占式调度,且在真正的实现中寄存于线程中,因此,系统开销极小,可以有效提高线程的任务并发性,而避免多线程的缺点。不过,目前,原生支持协程的语言还很少。Go语言中的goroutine是一种协程。

11.1.1 并发执行体之间的通信

执行体是个抽象的概念,在操作系统层面有多个概念与之对应,比如进程(process)、进程内的线程(thread)以及进程内的协程(coroutine)。

在工程上,有两种最常见的并发通信模型:“共享数据”和“消息”。

11.1.1.1 共享数据

共享数据是指多个并发单元分别保持对同一个数据的引用,实现对该数据的共享。被共享的数据可能有多种形式,比如内存数据块、磁盘文件等等。在实际工程应用中最常见的无疑是内存了,也就是常说的“共享内存”。

读写“共享内存”时往往需要加锁,这使得程序变得复杂。想象一下,在一个大的系统中具有无数的锁、无数的共享变量、无数的业务逻辑与错误处理分支,那将是一场噩梦。这噩梦就是众多C/C++开发者正在经历的,其实Java和C#开发者也好不到哪里去。

Go中提供了“锁”(如sync.Mutex和sync.RWMutex等)等机制来保障共享数据的读写正确。不过, Go语言中并不倡导通过共享数据来进行通信。Go语言中倡导使用消息机制来通信。Go语言共享数据理念:“不要通过共享内存来通信,而应该通过通信来共享内存。”

11.1.1.2 消息机制

消息机制认为每个并发单元是自包含的、独立的个体,并且都有自己的变量,但在不同并发单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息。不同并发单元间靠消息来通信,它们不会共享内存。

Go语言提供的消息通信机制被称为channel。

11.2 Goroutines (go)

A goroutine is a function that is capable of running concurrently with other functions. To create a goroutine we use the keyword go followed by a function invocation.

package main

import "fmt"

func f(n int) {
  for i := 0; i < 10; i++ {
    fmt.Println(n, ":", i)
  }
}

func main() {
  go f(0)
  go f(1)

  var input string
  fmt.Scanln(&input)      // 等待用户输入(防止main过早退出)
}

上面代码的输出不会每次一样。代码中有三个Goroutines,一个是 go f(0),另一个是 go f(1),还是一个隐含的main。

11.3 Channels (chan)

channel是Go在语言级别提供的goroutine间的通信方式。我们可以使用channel在两个或多个goroutine之间传递消息。

channel是类型相关的。也就是说,一个channel只能传递一种类型的值,这个类型需要在声明channel时指定。

11.3.1 基本语法

一般channel的声明形式为:

var chanName chan ElementType

与一般的变量声明不同的地方仅仅是在类型之前加了 chan 关键字。ElementType指定这个channel所能传递的元素类型。如,下面声明一个传递类型为int的channel:

var ch chan int         // 声明一个传递类型为int的channel

定义一个channel很简单,直接使用内置的函数 make() 即可。如:

ch := make(chan int)    // 声明并初始化了一个int型的名为ch的channel(不带缓冲的channel)

这就声明并初始化了一个int型的名为ch的channel。这是不带缓冲的channel,后文会介绍如何创建带缓冲的channel。

在channel的用法中,最常见的包括写入和读出。将一个数据写入(发送)至channel的语法很直观,如下:

ch <- value             // 将一个数据value“写入”到名为ch的channel中

对于不带缓冲的channel,向channel写入数据会导致程序阻塞(不管channel是否为空),直到有其他goroutine从这个channel中读取数据。

从channel中读取数据的语法是:

value := <-ch           // 从名为ch的channel中“读取”数据到value中
                        // 如果ch中没数据,会导致程序阻塞
                        // 如果ch中没数据,且被关闭,则程序不会阻塞,value为ch类型所对应的零值

如果channel ch中有数据就读出来保存到value中, 如果channel为空,那么从channel中读取数据会导致程序阻塞,直到channel中被写入数据或者channel被close为止。

11.3.2 select语句

早在Unix时代,select机制就已经被引入。通过调用select()函数来监控一系列的文件句柄,一旦其中一个文件句柄发生了I/O动作,该select()调用就会被返回。

Go语言直接在语言级别支持select关键字,用于处理异步I/O问题。

select的用法与switch语句非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。

不过, 在select中,每个case语句里必须是一个I/O操作。 其大致的结构如下:

select {
    case <-chan1:
    // 如果chan1成功读到数据,或者chan1被关闭,则进行该case处理语句
    case chan2 <- 1:
    // 如果成功向chan2写入数据,则进行该case处理语句
    default:
    // 如果上面都没有成功,则进入default处理流程。当省略default时,如果上面都没有成功,则select会阻塞
}

可以看出,select语句后面不接表达式(这和switch语句不同,switch语句后面可接也可不接表达式),而是直接去查看case语句。每个case语句都必须是一个面向channel的操作。上面的例子中,第一个case试图从chan1读取一个数据并直接忽略读到的数据,而第二个case则是试图向chan2中写入一个整型数1,如果这两者都没有成功,则到达default语句。

11.3.3 缓冲机制

之前我们示范创建的都是不带缓冲的channel,这种做法对于传递单个数据的场景可以接受。但对于需要持续传输大量数据的场景就有些不合适了。接下来我们介绍如何给channel带上缓冲,从而达到消息队列的效果。
要创建一个带缓冲的channel,在调用make()时将缓冲区大小作为第二个参数传入即可。如:

c := make(chan int, 1024)     // 创建带缓冲的channel

上面这个例子就创建了一个大小为1024的int类型channel,即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填完之前都不会阻塞。注:这和不带缓冲的channel是不同的,往不带缓冲的channel中写入数据都会阻塞(不管channel是否为空),直到其他goroutine从这个channel中读取数据。

c1 := make(chan int, 1)   // 带缓冲的channel,缓冲大小为1。channel为空时写操作不会阻塞
c2 := make(chan int)      // 不带缓冲的channel,和上行不同!不管channel是否为空写操作会阻塞!
c3 := make(chan int, 0)   // 不带缓冲的channel,和上行相同。

11.3.4 超时机制

在并发编程的通信过程中,往往需要处理超时问题,即向channel写数据时发现channel已满,或者从channel试图读取数据时发现channel为空。如果不正确处理这些情况,很可能会导致整个goroutine锁死。

Go语言没有提供直接的超时处理机制,但我们可以利用select机制。虽然select机制不是专为超时而设计的,却能很方便地解决超时问题。因为select的特点是只要其中一个case已经完成,程序就会继续往下执行,而不会考虑其他case的情况。

基于select此特性,我们来为channel实现超时机制:

// 首先,我们实现并执行一个匿名的超时等待函数
timeout := make(chan bool, 1)
go func() {
    time.Sleep(1 * time.Second) // 等待1秒钟
    timeout <- true
}()

// 然后我们把timeout这个channel利用起来
select {
    case <-ch:
    // 从ch中读取到数据
    case <-timeout:
    // 一直没有从ch中读取到数据,但从timeout中读取到了数据
}

这样使用select机制可以避免永久等待的问题,因为程序会在timeout中获取到一个数据后继续执行,无论对ch的读取是否还处于等待状态,从而达成1秒超时的效果。

使用 time.After 可以方便地得到类似上面例子中的timeout channel,如:

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"time"
)

func main() {
	response := make(chan *http.Response, 1)
	errors := make(chan *error)

	go func() {
		resp, err := http.Get("http://localhost:8000/")
		if err != nil {
			errors <- &err
		}
		response <- resp
	}()

	select {
	case r := <-response:
		defer r.Body.Close()
		body, _ := ioutil.ReadAll(r.Body)
		fmt.Printf("%s", body)
		return
	case err := <-errors:
		log.Fatal(*err)
	case <-time.After(200 * time.Millisecond):
		fmt.Printf("Timed out!")
		return
	}
}

11.3.5 单向channel

顾名思义,单向channel只能用于发送或者接收数据。不过,channel本身必然是同时支持读写的,否则根本没法用。假如一个channel真的只能读,那么肯定只会是空的,因为你没机会往里面写数据。同理,如果一个channel只允许写,即使写进去了,也没有丝毫意义,因为没有机会读取里面的数据。 所谓的单向channel概念,其实只是对channel的一种使用限制。

将一个channel变量传递到一个函数时,可以通过将其指定为单向channel变量,从而限制该函数中可以对此channel的操作,比如只能往这个channel写,或者只能从这个channel读。

单向channel的好处在于:使代码遵循“最小权限原则”,避免没必要地使用泛滥问题,进而导致程序失控。写过C++程序的读者肯定就会联想起const指针的用法。

单向channel变量的声明非常简单,如下:

var ch1 chan int        // ch1是一个正常的channel,不是单向的
var ch2 chan<- float64  // ch2是单向channel,只用于写float64数据
var ch3 <-chan int      // ch3是单向channel,只用于读取int数据

那么单向channel如何初始化呢?方式是对双向channel进行类型转换。如:

ch4 := make(chan int)
ch5 := <-chan int(ch4)    // ch5就是一个单向的读取channel
ch6 := chan<- int(ch4)    // ch6就是一个单向的写入channel

11.3.6 关闭channel

关闭channel非常简单,直接使用Go语言内置的 close() 函数即可:

close(ch)

如何判断一个channel是否已经被关闭?我们可以在读取的时候使用多重返回值的方式:

x, ok := <-ch

这个用法与map中的按键获取value的过程比较类似,只需要看第二个bool返回值即可,如果返回值是false则表示ch已经被关闭。

注1:“Only the sender should close a channel, never the receiver. Sending on a closed channel will cause a panic.”
注2:“It's OK to leave a Go channel open forever and never close it. When the channel is no longer used, it will be garbage collected.” 那什么时候需要关闭channel呢?参见节 11.3.7

11.3.7 range over channel


The loop for i := range ch receives values from the channel repeatedly until it is closed.

下面是 for ... range 操作channel的例子:

package main

import "fmt"

func main() {

    queue := make(chan string, 2)
    queue <- "one"
    queue <- "two"
    close(queue)

    // This `range` iterates over each element as it's
    // received from `queue`. Because we `close`d the
    // channel above, the iteration terminates after
    // receiving the 2 elements.
    for elem := range queue {
        fmt.Println(elem)
    }
}

下面是 for ... range 操作channel的另一个例子:

package main

import "fmt"

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)   // 会使 main 中的 for i := range c 结束
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    for i := range c {
        fmt.Println(i)
    }
}

注:如果上面函数fibonacci中没有 close(c) ,则函数main中的“for i := range c”不会结束,执行程序会提示下面错误:

$ go run 1.go
0
1
1
2
3
5
8
13
21
34
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        /Users/cig01/test/1.go:17 +0x97
exit status 2

11.3.8 实例:用channel实现goroutine同步

我们知道,语法 <-chanName 会一直阻塞当前goroutine直到通道chanName中有数据,或者chanName被关闭。利用这点可以实现goroutine同步,代码如下:

// We can use channels to synchronize execution
// across goroutines. Here's an example of using a
// blocking receive to wait for a goroutine to finish.

package main

import "fmt"
import "time"

// This is the function we'll run in a goroutine. The
// `done` channel will be used to notify another
// goroutine that this function's work is done.
func worker(done chan bool) {
    fmt.Print("working...")
    time.Sleep(time.Second)
    fmt.Println("done")

    // Send a value to notify that we're done.
    done <- true
}

func main() {

    // Start a worker goroutine, giving it the channel to
    // notify on.
    done := make(chan bool, 1)
    go worker(done)

    // Block until we receive a notification from the
    // worker on the channel.
    <-done
}

11.3.9 goroutine和channel应用实例:worker pool

下面是goroutine和channel的一个应用实例,摘自:https://gobyexample.com/worker-pools

// In this example we'll look at how to implement
// a _worker pool_ using goroutines and channels.

package main

import "fmt"
import "time"

// Here's the worker, of which we'll run several
// concurrent instances. These workers will receive
// work on the `jobs` channel and send the corresponding
// results on `results`. We'll sleep a second per job to
// simulate an expensive task.
func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Println("worker", id, "started  job", j)
        time.Sleep(1 * time.Second)
        fmt.Println("worker", id, "finished job", j, "result is", j * 2)
        results <- j * 2
    }
}

func main() {

    // In order to use our pool of workers we need to send
    // them work and collect their results. We make 2
    // channels for this.
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // This starts up 3 workers, initially blocked
    // because there are no jobs yet.
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Here we send 5 `jobs` and then `close` that
    // channel to indicate that's all the work we have.
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    // Finally we collect all the results of the work.
    finalResult := 0
    for a := 1; a <= 5; a++ {
        finalResult += <-results
    }
    fmt.Println("finalResult is", finalResult)
}

运行上面程序,输出:

worker 3 started  job 1
worker 1 started  job 2
worker 2 started  job 3
worker 1 finished job 2 result is 4
worker 1 started  job 4
worker 3 finished job 1 result is 2
worker 3 started  job 5
worker 2 finished job 3 result is 6
worker 3 finished job 5 result is 10
worker 1 finished job 4 result is 8
finalResult is 30

12 其它主题

12.1 import语句

假设你有包lib/math,其中导出了函数Sin。在不同的 import 方式下,使用函数Sin时的方式也不一样。如:

Import declaration          Local name of Sin

import   "lib/math"         math.Sin
import m "lib/math"         m.Sin
import . "lib/math"         Sin

参考:https://golang.org/ref/spec#Import_declarations

12.2 Type assertions

我们知道,空接口 interface{} 类型变量可以接受任意类型对象对其进行赋值。

下面是“Type assertions”语法,可以对变量 x 的类型进行断言。

t := x.(T)

如果变量 x 确实是类型 T ,则 x 会赋值给 t ;如果变量 x 不是类型 T ,则会引发panic。

下面形式的“Type assertions”不会引发panic。当变量 x 确实是类型 T 时,变量 ok 会是 true ,否则变量 okfalse

t, ok := x.(T)

12.3 Type switches


“Type switches”的语法如下所示,它和普通的switch语句(参见节 4.3 )类似。

package main

import "fmt"

func do(i interface{}) {
	switch v := i.(type) {     // This is 'Type switch'
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21)
	do("hello")
	do(true)
}

运行上面程序,会得到下面结果:

Twice 21 is 42
"hello" is 5 bytes long
I don't know about type bool!

13 环境配置

13.1 workspace

Go程序员往往把所有的Go代码放入到一个workspace中。

workspace一般包含下面三个目录:

src/     contains Go source files,
pkg/     contains package objects, and
bin/     contains executable commands.

下面是workspace目录结构的一个例子:

bin/
    hello                          # command executable
    outyet                         # command executable
pkg/
    linux_amd64/
        github.com/golang/example/
            stringutil.a           # package object
src/
    github.com/golang/example/
        .git/                      # Git repository metadata
        hello/
            hello.go               # command source
        outyet/
            main.go                # command source
            main_test.go           # test source
        stringutil/
            reverse.go             # package source
            reverse_test.go        # test source
    golang.org/x/image/
        .git/                      # Git repository metadata
        bmp/
            reader.go              # package source
            writer.go              # package source
    ... (many more repositories and packages omitted) ...

参考:
How to Write Go Code: https://golang.org/doc/code.html

13.1.1 GOPATH

GOPATH用于指定workspace的位置。

下面例子是设置GOPATH为$HOME/work:

export GOPATH=$HOME/work

go env GOPATH 可以查看当前GOPATH的设置。

13.2 交叉编译

Go内置了交叉编译功能。编译时只需要指定两个环境变量:GOOS和GOARCH,它们分别代表“Target Host OS”和“Target Host ARCH”,如果没有显式地设置这些环境变量,我们通过 go env GOOSgo env GOARCH 看到它们的当前值。

3 是go交叉编译所支持的组合。

Table 3: go交叉编译所支持的组合
$GOOS $GOARCH
android arm
darwin 386
darwin amd64
darwin arm
darwin arm64
dragonfly amd64
freebsd 386
freebsd amd64
freebsd arm
linux 386
linux amd64
linux arm
linux arm64
linux ppc64
linux ppc64le
linux mips
linux mipsle
linux mips64
linux mips64le
netbsd 386
netbsd amd64
netbsd arm
openbsd 386
openbsd amd64
openbsd arm
plan9 386
plan9 amd64
solaris amd64
windows 386
windows amd64

例如,我们可以在Linux(或Mac或其它系统)中编译能在windows平台下运行的64位程序,编译时指定 GOOS=windows GOARCH=amd64 即可:

$ GOOS=windows GOARCH=amd64 go build

参考:https://golang.org/doc/install/source#environment

14 Go语言标准库

Go语言的标准库提供了丰富的功能,能够满足大部分开发需求。完整列表可参考:https://golang.org/pkg/

14.1 实例:Http Server

下面是Go实现的一个简单的Http Server:

package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"net/http"
	"time"
)

var addr = flag.String("addr", ":8080", "http service address")

type Status struct {
	Cars []string `json:"cars"`
}

func handler1(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hi %s!", r.URL.Path)
}

// respond {"cars":["Mercedes","Honda","Toyota"]} or {"cars":["Ford","BMW"]}
func handler2(w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		data := Status{Cars: []string{"Ford", "BMW"}}
		if time.Now().UnixNano()%2 == 0 {
			data = Status{Cars: []string{"Mercedes", "Honda", "Toyota"}}
		}
		js, err := json.Marshal(data)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		w.Write(js)
	}
}

// start server:
// $ ./prog -addr ":9000"
// test:
// $ curl localhost:9000               # Hi /!
// $ curl localhost:9000/test          # {"cars":["Mercedes","Honda","Toyota"]} or {"cars":["Ford","BMW"]}
// $ curl localhost:9000/other/path    # Hi /other/path!
func main() {
	flag.Parse()
	http.HandleFunc("/", handler1)
	http.HandleFunc("/test", handler2)
	http.ListenAndServe(*addr, nil)
}

15 Tips

15.1 stack格式说明(panic输出格式)

当程序发生panic时,默认会打印出stack信息。注:你也可以使用 debug.PrintStack() 打印出stack信息。

下面通过一个例子来说明stack输出所包含的信息。

 1: package main
 2: 
 3: func main() {
 4:     slice := make([]string, 2, 4)
 5:     Example(slice, "hello", 10)
 6: }
 7: 
 8: func Example(slice []string, str string, i int) {
 9:     panic("Want stack trace")
10: }

上面程序会输出stack信息:

panic: Want stack trace

goroutine 1 [running]:
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)
	/Users/cig01/test/a.go:9 +0x39
main.main()
	/Users/cig01/test/a.go:5 +0x72
exit status 2

在上面输出中源码行号(如9和5)后面的数字(即“+0x39”和“+0x72”)表示“The address of the assembly instruction relative to the start of the function.”,即当前汇编指令相对于函数入口处的偏移。

下面我们重点关注函数Example的参数,它在声明时是3个参数,但stack输出中竟然有6个参数:

// Declaration
main.Example(slice []string, str string, i int)          // 源码中是3个参数

// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)   // stack输出中有6个参数

这是因为:

  • Slices are three words (a pointer to a backing array, a length, and a capacity)
  • Strings are two words (a pointer to string data and a length)

12 直观地演示了slice和string展开为多个参数的过程。

go_stack_arg_slice.png

Figure 1: 1个slice参数在stack输出中展开为3个参数

go_stack_arg_string.png

Figure 2: 1个string参数在stack输出中展开为2个参数

参考:
Stack Traces In Go
Understanding Go panic output

15.1.1 String, Slice, Interface类型的参数都会展开为多个参数

前面介绍了String和Slice类型的参数在输出stack时会展开为多个参数。除此外,Interface类型的参数也会展开为多个参数,具体地说会展开为两个参数:a pointer to the type and a pointer to the value.

下面的例子演示了Interface类型的参数在输出stack时会展开为2个参数。

package main

import "fmt"

/* 定义接口,其名为geometry,它包含两个方法 */
type geometry interface {
    area() float64                // 计算面积
    perimeter() float64           // 计算周长
}

/* 定义长方形,你还可以定义圆形等 */
type rect struct {
    width, height float64
}

/* 实现长方形的接口函数 */
func (r rect) area() float64 {
    return r.width * r.height
}
func (r rect) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func measure(g geometry) {  // 其参数是一个接口
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
	panic("Just panic")
}

func main() {
    r := rect{width: 3, height: 4}
    measure(r)   // 输出长方形的面积和周长
}

上面程序输出:

{3 4}
12
14
panic: Just panic

goroutine 1 [running]:
main.measure(0x10d2500, 0xc420084010)   // measure参数变为两个(但源码中是一个接口)
	/Users/cig01/test/1.go:28 +0x183
main.main()
	/Users/cig01/test/1.go:33 +0x6b
exit status 2

15.2 new()和make()的区别

new() 分配对象内存,返回对象指针。
make() 用来为slice,map或chan类型分配内存和初始化对象(注:它只能用在这三种类型上), make() 返回的是类型的引用而不是指针。

15.3 i++是语句(不是表达式)

在golang中 ++ , -- 操作是语句而不是表达式。所以 a=b++, return x++ 等写法会提示错误,语句无法放到表达式位置。如:

// 下面程序是错误的!不能通过编译
package main

import "fmt"

func main() {
	a := 10
	b := a++                // syntax error: unexpected ++ at end of statement
	fmt.Printf("%d%d", a++) // syntax error: unexpected ++, expecting comma or )
	fmt.Printf("%d", b)
}

参考:Why are ++ and – statements and not expressions? And why postfix, not prefix?

15.4 静态编译

Go采用静态编译,把其runtime嵌入到了每一个可执行文件中,这使得Go的可执行程序一般比较大。这使程序的部署变得非常简单,直接把程序复制过去即可,再也不用关心各种依赖库了。


Author: cig01

Created: <2015-10-03 Sat 00:00>

Last updated: <2018-06-22 Fri 01:17>

Creator: Emacs 25.3.1 (Org mode 9.1.4)