Go By Example | Go语言独家授权课程
Go
MMcGranaghan
Developer platform @stripe
3人收藏 4169次学习

Go By Example | Go语言独家授权课程

1KE请来了GoByExample的作者——硅谷小鲜肉Mark McGranaghan来为我们讲解萌萌哒GO语言!

欢迎大家戳他的Git主页:https://github.com/mmcgrana/gobyexample

Go 是一个被设计用来建立简单,快速和可信赖的软件的开源程序设计语言。

Go by Example 是一个实践性的通过带注释的例子程序去介绍 Go。让我们先来围观Go的主页长啥样:

接下来我们就通过一个个小例子来让你一口气学会GO吧!

Hello, World

我们的第一个程序将打印传说中的 "hello world"消息,右边是完整的程序代码。

package main
import "fmt"
func main() {
fmt.Println("hello world")
}

要运行这个程序,将这些代码放到 hello-world.go 中并且使用 go run 命令。

$ go run hello-world.go
hello world

有时候我们想将我们的程序编译成二进制文件。我们可以通过 go build 命来达到目的。

$ go build hello-world.go
$ ls
hello-world	hello-world.go

然后我们可以直接运行这个二进制文件。

$ ./hello-world
hello world

现在我们可以运行和并以基本的 Go 程序,让我们学习更多的关于这门语言的知识吧。

Go 拥有各值类型,包括字符串,整形,浮点型,布尔型等。下面是一些基本的例子。

package main
import "fmt"
func main() {
    fmt.Println("go" + "lang")
    //字符串可以通过 + 连接。

    fmt.Println("1+1 =", 1+1)
    fmt.Println("7.0/3.0 =", 7.0/3.0)
    //整数和浮点数

    fmt.Println(true && false)
    fmt.Println(true || false)
    fmt.Println(!true)
    //布尔型,还有你想要的逻辑运算符。
}
$ go run values.go
golang
1+1 = 2
7.0/3.0 = 2.3333333333333335
false
true
false

变量

在 Go 中,变量 被显式声明,并被编译器所用来检查函数调用时的类型正确性。

package main

import "fmt"

func main() {

    var a string = "initial"
    fmt.Println(a)
    //var 声明 1 个或者多个变量。

    var b, c int = 1, 2
    fmt.Println(b, c)
    //你可以申明一次性声明多个变量。

    var d = true
    fmt.Println(d)
    //Go 将自动推断已经初始化的变量类型。
			
    var e int
    fmt.Println(e)
    //声明变量且没有给出对应的初始值时,变量将会初始化为零值。例如,一个int的零值是0。
			
    f := "short"
    fmt.Println(f)
    //:= 语句是申明并初始化变量的简写,例如这个例子中的 var f string = "short"。
}
$ go run variables.go
initial
1 2
true
0
short

常量

Go 支持字符、字符串、布尔和数值 常量 。

package main

import "fmt"
import "math"

const s string = "constant"
//const 用于声明一个常量。

func main() {
    fmt.Println(s)
    //const 语句可以出现在任何 var 语句可以出现的地方

    const n = 500000000
    //常数表达式可以执行任意精度的运算

    const d = 3e20 / n
    fmt.Println(d)
    //数值型常量是没有确定的类型的,直到它们被给定了一个类型,比如说一次显示的类型转化。

    fmt.Println(int64(d))
    //当上下文需要时,一个数可以被给定一个类型,比如变量赋值或者函数调用。举个例子,这里的 math.Sin函数需要一个 float64 的参数。

    fmt.Println(math.Sin(n))
}
$ go run constant.go 
constant
6e+11
600000000000
-0.28470407323754404

For循环

for 是 Go 中唯一的循环结构。这里有 for 循环的三个基本使用方式。

package main

import "fmt"

func main() {

    i := 1
    for i <= 3 {
        fmt.Println(i)
        i = i + 1
    }
    //最常用的方式,带单个循环条件。

    for j := 7; j <= 9; j++ {
        fmt.Println(j)
    }
    //经典的初始化/条件/后续形式 for 循环。

    for {
        fmt.Println("loop")
        break
    }
    //不带条件的 for 循环将一直执行,直到在循环体内使用了 break 或者 return 来跳出循环。
}
$ go run for.go
1
2
3
7
8
9
loop

在教程后面,当我们学到 rang 语句,channels,以及其他数据结构时,将会看到一些 for 的其它使用形式。

if/else 分支

if 和 else 分支结构在 Go 中当然是直接了当的了。

package main

import "fmt"

func main() {

    if 7%2 == 0 {
        fmt.Println("7 is even")
    } else {
        fmt.Println("7 is odd")
    }
    //这里是一个基本的例子。

    if 8%4 == 0 {
        fmt.Println("8 is divisible by 4")
    }
    //你可以不要 else 只用 if 语句。

    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")
    }
    //在条件语句之前可以有一个语句;任何在这里声明的变量都可以在所有的条件分支中使用。
}

注意,在 Go 中,你可以不适用圆括号,但是花括号是需要的。

$ go run if-else.go 
7 is odd
8 is divisible by 4
9 has 1 digit

Go 里没有三目运算符,所以即使你只需要基本的条件判断,你仍需要使用完整的 if 语句。

分支结构

switch ,方便的条件分支语句。

package main

import "fmt"
import "time"

func main() {

    i := 2
    fmt.Print("write ", i, " as ")
    switch i {
    case 1:
        fmt.Println("one")
    case 2:
        fmt.Println("two")
    case 3:
        fmt.Println("three")
    }
    //一个基本的 switch。

    switch time.Now().Weekday() {
    case time.Saturday, time.Sunday:
        fmt.Println("it's the weekend")
    default:
        fmt.Println("it's a weekday")
    }
    //在一个 case 语句中,你可以使用逗号来分隔多个表达式。在这个例子中,我们很好的使用了可选的default 分支

    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("it's before noon")
    default:
        fmt.Println("it's after noon")
    }
    //不带表达式的 switch 是实现 if/else 逻辑的另一种方式。这里展示了 case 表达式是如何使用非常量的。
}
$ go run switch.go 
write 2 as two
it's the weekend
it's before noon

数组

在 Go 中,数组 是一个固定长度的数列。

package main

import "fmt"

func main() {

    var a [5]int
    fmt.Println("emp:", a)
    //这里我们创建了一个数组 a 来存放刚好 5 个 int。元素的类型和长度都是数组类型的一部分。数组默认是零值的,对于 int 数组来说也就是 0。

    a[4] = 100
    fmt.Println("set:", a)
    fmt.Println("get:", a[4])
    //我们可以使用 array[index] = value 语法来设置数组指定位置的值,或者用 array[index] 得到值。

    fmt.Println("len:", len(a))
    //使用内置函数 len 返回数组的长度

    b := [5]int{1, 2, 3, 4, 5}
    fmt.Println("dcl:", b)
    //使用这个语法在一行内初始化一个数组

    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)
}

注意,在使用 fmt.Println 来打印数组的时候,会使用[v1 v2 v3 ...] 的格式显示。

$ go run arrays.go
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]]

在典型的 Go 程序中,相对于数组而言,slice 使用的更多。我们将在后面讨论 slice。

切片

Slice 是 Go 中一个关键的数据类型,是一个比数组更加强大的序列接口。

package main

import "fmt"

func main() {

    s := make([]string, 3)
    fmt.Println("emp:", s)
    //不像数组,slice 的类型仅有它所包含的元素决定(不像数组中还需要元素的个数)。要创建一个长度非零的空slice,需要使用内建的方法 make。这里我们创建了一个长度为3的 string 类型 slice(初始化为零值)。

    //我们可以和数组一下设置和得到值
    s[0] = "a"
    s[1] = "b"
    s[2] = "c"
    fmt.Println("set:", s)
    fmt.Println("get:", s[2])

    fmt.Println("len:", len(s))
    //如你所料,len 返回 slice 的长度

    s = append(s, "d")
    s = append(s, "e", "f")
    fmt.Println("apd:", s)
    //作为基本操作的补充,slice 支持比数组更多的操作。其中一个是内建的 append,它返回一个包含了一个或者多个新值的 slice。注意我们接受返回由 append返回的新的 slice 值。

    c := make([]string, len(s))
    copy(c, s)
    fmt.Println("cpy:", c)
    //Slice 也可以被 copy。这里我们创建一个空的和 s 有相同长度的 slice c,并且将 s 复制给 c。

    l := s[2:5]
    fmt.Println("sl1:", l)
    //Slice 支持通过 slice[low:high] 语法进行“切片”操作。例如,这里得到一个包含元素 s[2], s[3],s[4] 的 slice。

    l = s[:5]
    fmt.Println("sl2:", l)
    //这个 slice 从 s[0] 到(但是包含)s[5]。

    l = s[2:]
    fmt.Println("sl3:", l)
    //这个 slice 从(包含)s[2] 到 slice 的后一个值。

    t := []string{"g", "h", "i"}
    fmt.Println("dcl:", t)
    //我们可以在一行代码中申明并初始化一个 slice 变量。

    twoD := make([][]int, 3)
    //Slice 可以组成多维数据结构。内部的 slice 长度可以不同,这和多位数组不同。

    for i := 0; i < 3; i++ {
        innerLen := i + 1
        twoD[i] = make([]int, innerLen)
        for j := 0; j < innerLen; j++ {
            twoD[i][j] = i + j
        }
    }
    fmt.Println("2d: ", twoD)
}

注意,slice 和数组不同,虽然它们通过 fmt.Println 输出差不多。

$ go run slices.go
emp: [  ]
set: [a b c]
get: c
len: 3
apd: [a b c d e f]
cpy: [a b c d e f]
sl1: [c d e]
sl2: [a b c d e]
sl3: [c d e f]
dcl: [g h i]
2d:  [[0] [1 2] [2 3 4]]

看看这个由 Go 团队撰写的一篇很棒的博文,获得更多关于 Go 中 slice 的设计和实现细节。

现在,我们已经看过了数组和 slice,接下来我们将看看 Go 中的另一个关键的内建数据类型:map。

关联数组

map 是 Go 内置关联数据类型(在一些其他的语言中称为哈希 或者字典 )。

package main

import "fmt"

func main() {

    m := make(map[string]int)
    //要创建一个空 map,需要使用内建的 make:make(map[key-type]val-type).

    m["k1"] = 7
    m["k2"] = 13
    //使用典型的 make[key] = val 语法来设置键值对。

    fmt.Println("map:", m)
    //使用例如 Println 来打印一个 map 将会输出所有的键值对。

    v1 := m["k1"]
    fmt.Println("v1: ", v1)
    //使用 name[key] 来获取一个键的值

    fmt.Println("len:", len(m))
    //当对一个 map 调用内建的 len 时,返回的是键值对数目

    delete(m, "k2")
    fmt.Println("map:", m)
    //内建的 delete 可以从一个 map 中移除键值对

    _, prs := m["k2"]
    fmt.Println("prs:", prs)
    //当从一个 map 中取值时,可选的第二返回值指示这个键是在这个 map 中。这可以用来消除键不存在和键有零值,像 0 或者 "" 而产生的歧义。

    n := map[string]int{"foo": 1, "bar": 2}
    fmt.Println("map:", n)
    //你也可以通过这个语法在同一行申明和初始化一个新的map。
}

注意一个 map 在使用 fmt.Println 打印的时候,是以map[k:v k:v]的格式输出的。

$ go run maps.go 
map: map[k1:7 k2:13]
v1:  7
len: 2
map: map[k1:7]
prs: false
map: map[foo:1 bar:2]

 

Range 遍历

range 迭代各种各样的数据结构。让我们来看看如何在我们已经学过的数据结构上使用 rang 吧。

package main

import "fmt"

func main() {

    nums := []int{2, 3, 4}
    sum := 0
    for _, num := range nums {
        sum += num
    }
    fmt.Println("sum:", sum)
    //这里我们使用 range 来统计一个 slice 的元素个数。数组也可以采用这种方法。

    for i, num := range nums {
        if num == 3 {
            fmt.Println("index:", i)
        }
    }
    //range 在数组和 slice 中都同样提供每个项的索引和值。上面我们不需要索引,所以我们使用空值定义符_ 来忽略它。有时候我们实际上是需要这个索引的。

    kvs := map[string]string{"a": "apple", "b": "banana"}
    for k, v := range kvs {
        fmt.Printf("%s -> %s\n", k, v)
    }
    //range 在 map 中迭代键值对。

    for i, c := range "go" {
        fmt.Println(i, c)
    }
    //range 在字符串中迭代 unicode 编码。第一个返回值是rune 的起始字节位置,然后第二个是 rune 自己。
}
$ go run range.go 
sum: 9
index: 1
a -> apple
b -> banana
0 103
1 111

函数

函数 是 Go 的中心。我们将通过一些不同的例子来进行学习。

package main

import "fmt"

func plus(a int, b int) int {
    //这里是一个函数,接受两个 int 并且以 int 返回它们的和

    return a + b
}
    //Go 需要明确的返回值,例如,它不会自动返回最后一个表达式的值

func main() {

    res := plus(1, 2)
    fmt.Println("1+2 =", res)
    //正如你期望的那样,通过 name(args) 来调用一个函数
}
$ go run functions.go 
1+2 = 3

这里有许多 Go 函数的其他特性。其中一个就是多值返回,也是我们接下来需要接触的。

多返回值

Go 内建多返回值 支持。这个特性在 Go 语言中经常被用到,例如用来同时返回一个函数的结果和错误信息。

package main

import "fmt"

func vals() (int, int) {
    return 3, 7
}
//(int, int) 在这个函数中标志着这个函数返回 2 个 int。

func main() {

    a, b := vals()
    fmt.Println(a)
    fmt.Println(b)
//这里我们通过多赋值 操作来使用这两个不同的返回值。

    _, c := vals()
    fmt.Println(c)
//如果你仅仅想返回值的一部分的话,你可以使用空白定义符 _。
}
$ go run multiple-return-values.go
3
7
7

允许可变长参数是 Go 函数的另一个好的特性;我们将在接下来进行学习。

变参函数

可变参数函数。可以用任意数量的参数调用。例如,fmt.Println 是一个常见的变参函数。

package main

import "fmt"

func sum(nums ...int) {
    fmt.Print(nums, " ")
    total := 0
    for _, num := range nums {
        total += num
    }
    fmt.Println(total)
}
//这个函数使用任意数目的 int 作为参数。

func main() {

    sum(1, 2)
    sum(1, 2, 3)
//变参函数使用常规的调用方式,除了参数比较特殊。

    nums := []int{1, 2, 3, 4}
    sum(nums...)
//如果你的 slice 已经有了多个值,想把它们作为变参使用,你要这样调用 func(slice...)。
}
$ go run variadic-functions.go 
[1 2] 3
[1 2 3] 6
[1 2 3 4] 10

Go 函数的另一个关键的方面是闭包结构,这是接下来我们需要看看的。

闭包

Go 支持通过 闭包来使用 匿名函数。匿名函数在你想定义一个不需要命名的内联函数时是很实用的。

package main

import "fmt"

func intSeq() func() int {
    i := 0
    return func() int {
        i += 1
        return i
    }
}
//这个 intSeq 函数返回另一个在 intSeq 函数体内定义的匿名函数。这个返回的函数使用闭包的方式 隐藏 变量 i。

func main() {

    nextInt := intSeq()
//我们调用 intSeq 函数,将返回值(也是一个函数)赋给nextInt。这个函数的值包含了自己的值 i,这样在每次调用 nextInt 是都会更新 i 的值。

    fmt.Println(nextInt())
    fmt.Println(nextInt())
    fmt.Println(nextInt())
//通过多次调用 nextInt 来看看闭包的效果。

    newInts := intSeq()
    fmt.Println(newInts())
}

为了确认这个状态对于这个特定的函数是唯一的,我们重新创建并测试一下。

$ go run closures.go
1
2
3
1

我们马上要学习关于函数的最后一个特性,递归啦。

递归

Go 支持 递归。这里是一个经典的阶乘示例。

package main

import "fmt"

func fact(n int) int {
    if n == 0 {
        return 1
    }
    return n * fact(n-1)
}
//face 函数在到达 face(0) 前一直调用自身。

func main() {
    fmt.Println(fact(7))
}
$ go run recursion.go 
5040

指针

Go 支持 指针,允许在程序中通过引用传递值或者数据结构。

package main

import "fmt"

func zeroval(ival int) {
    ival = 0
}
//我们将通过两个函数:zeroval 和 zeroptr 来比较指针和值类型的不同。zeroval 有一个 int 型参数,所以使用值传递。zeroval 将从调用它的那个函数中得到一个 ival形参的拷贝。

func zeroptr(iptr *int) {
    *iptr = 0
}
//zeroptr 有一和上面不同的 *int 参数,意味着它用了一个 int指针。函数体内的 *iptr 接着解引用 这个指针,从它内存地址得到这个地址对应的当前值。对一个解引用的指针赋值将会改变这个指针引用的真实地址的值。

func main() {
    i := 1
    fmt.Println("initial:", i)

    zeroval(i)
    fmt.Println("zeroval:", i)

    zeroptr(&i)
    //通过 &i 语法来取得 i 的内存地址,例如一个变量i 的指针。

    fmt.Println("zeroptr:", i)
    fmt.Println("pointer:", &i)
    //指针也是可以被打印的。

}

zeroval 在 main 函数中不能改变 i 的值,但是zeroptr 可以,因为它有一个这个变量的内存地址的引用。

$ go run pointers.go
initial: 1
zeroval: 1
zeroptr: 0
pointer: 0x42131100

结构体

Go 的结构体 是各个字段字段的类型的集合。这在组织数据时非常有用。

package main

import "fmt"

type person struct {
    name string
    age  int
}
//这里的 person 结构体包含了 name 和 age 两个字段。

func main() {

    fmt.Println(person{"Bob", 20})
    //使用这个语法创建了一个新的结构体元素。

    fmt.Println(person{name: "Alice", age: 30})
    //你可以在初始化一个结构体元素时指定字段名字。

    fmt.Println(person{name: "Fred"})
    //省略的字段将被初始化为零值。

    fmt.Println(&person{name: "Ann", age: 40})
    //& 前缀生成一个结构体指针。

    s := person{name: "Sean", age: 50}
    fmt.Println(s.name)
    //使用点来访问结构体字段。

    sp := &s
    fmt.Println(sp.age)
    //也可以对结构体指针使用. - 指针会被自动解引用。

    sp.age = 51
    fmt.Println(sp.age)
    //结构体是可变的。
}
$ go run structs.go
{Bob 20}
{Alice 30}
{Fred 0}
&{Ann 40}
Sean
50
51

方法

Go 支持在结构体类型中定义方法 

package main

import "fmt"

type rect struct {
    width, height int
}

func (r *rect) area() int {
    return r.width * r.height
}
//这里的 area 方法有一个接收器类型 rect。

func (r rect) perim() int {
    return 2*r.width + 2*r.height
}
//可以为值类型或者指针类型的接收器定义方法。这里是一个值类型接收器的例子。

func main() {
    r := rect{width: 10, height: 5}

    fmt.Println("area: ", r.area())
    fmt.Println("perim:", r.perim())
    //这里我们调用上面为结构体定义的两个方法。

    rp := &r
    fmt.Println("area: ", rp.area())
    fmt.Println("perim:", rp.perim())
}

Go 自动处理方法调用时的值和指针之间的转化。你可以使用指针来调用方法来避免在方法调用时产生一个拷贝,或者让方法能够改变接受的数据。

$ go run methods.go 
area:  50
perim: 30
area:  50
perim: 30

接下来我们将 Go 语言中组织和命名相关的方法集合的机制:接口。

接口

接口 是方法特征的命名集合。

package main

import "fmt"
import "math"

type geometry interface {
    area() float64
    perim() float64
}
//这里是一个几何体的基本接口。

type square struct {
    width, height float64
}
type circle struct {
    radius float64
}
//在我们的例子中,我们将让 square 和 circle 实现这个接口

func (s square) area() float64 {
    return s.width * s.height
}
func (s square) perim() float64 {
    return 2*s.width + 2*s.height
}
//要在 Go 中实现一个接口,我们只需要实现接口中的所有方法。这里我们让 square 实现了 geometry 接口。

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
    return 2 * math.Pi * c.radius
}
//circle 的实现。

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perim())
}
//如果一个变量的是接口类型,那么我们可以调用这个被命名的接口中的方法。这里有一个一通用的 measure 函数,利用这个特性,它可以用在任何 geometry 上。

func main() {
    s := square{width: 3, height: 4}
    c := circle{radius: 5}
//结构体类型 circle 和 square 都实现了 geometry接口,所以我们可以使用它们的实例作为 measure 的参数。

    measure(s)
    measure(c)
}
$ go run interfaces.go
{3 4}
12
14
{5}
78.53981633974483
31.41592653589793

要学习更多关于 Go 的接口的知识,看看这篇很棒的博文

错误处理

Go 语言使用一个独立的·明确的返回值来传递错误信息的。这与使用异常的 Java 和 Ruby 以及在 C 语言中经常见到的超重的单返回值/错误值相比,Go 语言的处理方式能清楚的知道哪个函数返回了错误,并能想调用那些没有出错的函数一样调用。

package main

import "errors"
import "fmt"

func f1(arg int) (int, error) {
    if arg == 42 {
    //按照惯例,错误通常是最后一个返回值并且是 error 类型,一个内建的接口。

        return -1, errors.New("can't work with 42")
        //errors.New 构造一个使用给定的错误信息的基本error 值。
    }

    return arg + 3, nil
    //返回错误值为 nil 代表没有错误。
}
//通过实现 Error 方法来自定义 error 类型是可以得。这里使用自定义错误类型来表示上面的参数错误。

type argError struct {
    arg  int
    prob string
}

func (e *argError) Error() string {
    return fmt.Sprintf("%d - %s", e.arg, e.prob)
}

func f2(arg int) (int, error) {
    if arg == 42 {

        return -1, &argError{arg, "can't work with it"}
    }
    return arg + 3, nil
}
//在这个例子中,我们使用 &argError 语法来建立一个新的结构体,并提供了 arg 和 prob 这个两个字段的值。

func main() {

    for _, i := range []int{7, 42} {
        if r, e := f1(i); e != nil {
            fmt.Println("f1 failed:", e)
        } else {
            fmt.Println("f1 worked:", r)
        }
    }
    for _, i := range []int{7, 42} {
        if r, e := f2(i); e != nil {
            fmt.Println("f2 failed:", e)
        } else {
            fmt.Println("f2 worked:", r)
        }
    }
    //下面的两个循环测试了各个返回错误的函数。注意在 if行内的错误检查代码,在 Go 中是一个普遍的用法。

    _, e := f2(42)
    if ae, ok := e.(*argError); ok {
        fmt.Println(ae.arg)
        fmt.Println(ae.prob)
    }
    //你如果想在程序中使用一个自定义错误类型中的数据,你需要通过类型断言来得到这个错误类型的实例。
}
$ go run errors.go
f1 worked: 10
f1 failed: can't work with 42
f2 worked: 10
f2 failed: 42 - can't work with it
42
can't work with it

到 Go 博客去看这篇很棒的文章获取更多关于错误处理的信息。

协程

Go 协程 在执行上来说是轻量级的线程。

package main

import "fmt"

func f(from string) {
    for i := 0; i < 3; i++ {
        fmt.Println(from, ":", i)
    }
}

func main() {

    f("direct")
    //假设我们有一个函数叫做 f(s)。我们使用一般的方式调并同时运行。

    go f("goroutine")
    //使用 go f(s) 在一个 Go 协程中调用这个函数。这个新的 Go 协程将会并行的执行这个函数调用。

    go func(msg string) {
        fmt.Println(msg)
    }("going")
    //你也可以为匿名函数启动一个 Go 协程。

    var input string
    fmt.Scanln(&input)
    fmt.Println("done")
    //现在这两个 Go 协程在独立的 Go 协程中异步的运行,所以我们需要等它们执行结束。这里的 Scanln 代码需要我们在程序退出前按下任意键结束。
}

当我们运行这个程序时,将首先看到阻塞式调用的输出,然后是两个 Go 协程的交替输出。这种交替的情况表示 Go 运行时是以异步的方式运行协程的。

$ go run goroutines.go
direct : 0
direct : 1
direct : 2
goroutine : 0
going
goroutine : 1
goroutine : 2
<enter>
done

接下来我们将学习在并发的 Go 程序中的 Go 协程的辅助特性:通道。

通道

通道 是连接多个 Go 协程的管道。你可以从一个 Go 协程将值发送到通道,然后在别的 Go 协程中接收。

package main

import "fmt"

func main() {

    messages := make(chan string)
    //使用 make(chan val-type) 创建一个新的通道。通道类型就是他们需要传递值的类型。

    go func() { messages <- "ping" }()
    //使用 channel <- 语法 发送 一个新的值到通道中。这里我们在一个新的 Go 协程中发送 "ping" 到上面创建的messages 通道中。

    msg := <-messages
    fmt.Println(msg)
    //使用 <-channel 语法从通道中 接收 一个值。这里将接收我们在上面发送的 "ping" 消息并打印出来。
}

我们运行程序时,通过通道,消息 "ping" 成功的从一个 Go 协程传到另一个中。

$ go run channels.go
ping

默认发送和接收操作是阻塞的,直到发送方和接收方都准备完毕。这个特性允许我们,不使用任何其它的同步操作,来在程序结尾等待消息 "ping"。

通道缓冲

默认通道是 无缓冲 的,这意味着只有在对应的接收(<- chan)通道准备好接收时,才允许进行发送(chan <-)。可缓存通道允许在没有对应接收方的情况下,缓存限定数量的值。

package main

import "fmt"

func main() {

    messages := make(chan string, 2)
    //这里我们 make 了一个通道,最多允许缓存 2 个值。

    messages <- "buffered"
    messages <- "channel"
    //因为这个通道是有缓冲区的,即使没有一个对应的并发接收方,我们仍然可以发送这些值。

    fmt.Println(<-messages)
    fmt.Println(<-messages)
    //然后我们可以像前面一样接收这两个值。
}
$ go run channel-buffering.go 
buffered
channel

通道同步

我们可以使用通道来同步 Go 协程间的执行状态。这里是一个使用阻塞的接受方式来等待一个 Go 协程的运行结束。

package main

import "fmt"
import "time"

func worker(done chan bool) {
    fmt.Print("working...")
    time.Sleep(time.Second)
    fmt.Println("done")
    //这是一个我们将要在 Go 协程中运行的函数。done 通道将被用于通知其他 Go 协程这个函数已经工作完毕。

    done <- true
    //发送一个值来通知我们已经完工啦。
}

func main() {

    done := make(chan bool, 1)
    go worker(done)
    //运行一个 worker Go协程,并给予用于通知的通道。
    //程序将在接收到通道中 worker 发出的通知前一直阻塞。

    <-done
}
$ go run channel-synchronization.go
working...done

如果你把 <- done 这行代码从程序中移除,程序甚至会在worker还没开始运行时就结束了。

通道方向

当使用通道作为函数的参数时,你可以指定这个通道是不是只用来发送或者接收值。这个特性提升了程序的类型安全性。

package main

import "fmt"

func ping(pings chan<- string, msg string) {
    pings <- msg
}
//ping 函数定义了一个只允许发送数据的通道。尝试使用这个通道来接收数据将会得到一个编译时错误。

func pong(pings <-chan string, pongs chan<- string) {
    msg := <-pings
    pongs <- msg
}
//pong 函数允许通道(pings)来接收数据,另一通道(pongs)来发送数据。

func main() {
    pings := make(chan string, 1)
    pongs := make(chan string, 1)
    ping(pings, "passed message")
    pong(pings, pongs)
    fmt.Println(<-pongs)
}
$ go run channel-directions.go
passed message

通道选择器

Go 的通道选择器 让你可以同时等待多个通道操作。Go 协程和通道以及选择器的结合是 Go 的一个强大特性。

package main

import "time"
import "fmt"

func main() {
//在我们的例子中,我们将从两个通道中选择。

    c1 := make(chan string)
    c2 := make(chan string)
    //各个通道将在若干时间后接收一个值,这个用来模拟例如并行的 Go 协程中阻塞的 RPC 操作

    go func() {
        time.Sleep(time.Second * 1)
        c1 <- "one"
    }()
    go func() {
        time.Sleep(time.Second * 2)
        c2 <- "two"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }
    //我们使用 select 关键字来同时等待这两个值,并打印各自接收到的值。
}

我们首先接收到值 "one",然后就是预料中的 "two"了。

$ time go run select.go 
received one
received two

real	0m2.245s

注意从第一次和第二次 Sleep 并发执行,总共仅运行了两秒左右。

超时处理

超时 对于一个连接外部资源,或者其它一些需要花费执行时间的操作的程序而言是很重要的。得益于通道和select,在 Go中实现超时操作是简洁而优雅的。

package main

import "time"
import "fmt"

func main() {

    c1 := make(chan string, 1)
    go func() {
        time.Sleep(time.Second * 2)
        c1 <- "result 1"
    }()
    //在我们的例子中,假如我们执行一个外部调用,并在 2 秒后通过通道 c1 返回它的执行结果

    select {
    case res := <-c1:
        fmt.Println(res)
    case <-time.After(time.Second * 1):
        fmt.Println("timeout 1")
    }
    //这里是使用 select 实现一个超时操作。res := <- c1 等待结果,<-Time.After 等待超时时间 1 秒后发送的值。由于 select 默认处理第一个已准备好的接收操作,如果这个操作超过了允许的 1 秒的话,将会执行超时 case。

    c2 := make(chan string, 1)
    go func() {
        time.Sleep(time.Second * 2)
        c2 <- "result 2"
    }()
    select {
    case res := <-c2:
        fmt.Println(res)
    case <-time.After(time.Second * 3):
        fmt.Println("timeout 2")
    }
    //如果我允许一个长一点的超时时间 3 秒,将会成功的从 c2接收到值,并且打印出结果。
}

运行这个程序,首先显示运行超时的操作,然后是成功接收的。

$ go run timeouts.go 
timeout 1
result 2

使用这个 select 超时方式,需要使用通道传递结果。这对于一般情况是个好的方式,因为其他重要的 Go 特性是基于通道和select 的。接下来我们就要看到两个例子:timerticker

非阻塞通道操作

常规的通过通道发送和接收数据是阻塞的。然而,我们可以使用带一个 default 子句的 select 来实现非阻塞 的发送、接收,甚至是非阻塞的多路 select

package main

import "fmt"

func main() {
    messages := make(chan string)
    signals := make(chan bool)

    select {
    case msg := <-messages:
        fmt.Println("received message", msg)
    default:
        fmt.Println("no message received")
    }
    //这里是一个非阻塞接收的例子。如果在message中存在,然后 select 将这个值带入 <-messages case中。如果不是,就直接到 default 分支中。

    msg := "hi"
    select {
    case messages <- msg:
        fmt.Println("sent message", msg)
    default:
        fmt.Println("no message sent")
    }
    //一个非阻塞发送的实现方法和上面一样。

    select {
    case msg := <-messages:
        fmt.Println("received message", msg)
    case sig := <-signals:
        fmt.Println("received signal", sig)
    default:
        fmt.Println("no activity")
    }
    //我们可以在 default 前使用多个 case 子句来实现一个多路的非阻塞的选择器。这里我们视图在 messages和 signals 上同时使用非阻塞的接受操作。
}
$ go run non-blocking-channel-operations.go 
no message received
no message sent
no activity
 

通道的关闭

关闭 一个通道意味着不能再向这个通道发送值了。这个特性可以用来给这个通道的接收方传达工作已将完成的信息。

package main

import "fmt"

func main() {
    jobs := make(chan int, 5)
    done := make(chan bool)
    //在这个例子中,我们将使用一个 jobs 通道来传递 main() 中 Go协程任务执行的结束信息到一个工作 Go 协程中。当我们没有多余的任务给这个工作 Go 协程时,我们将 close 这个 jobs 通道。

    go func() {
        for {
            j, more := <-jobs
            if more {
                fmt.Println("received job", j)
            } else {
                fmt.Println("received all jobs")
                done <- true
                return
            }
        }
    }()
    //这是工作 Go 协程。使用 j, more := <- jobs 循环的从jobs 接收数据。在接收的这个特殊的二值形式的值中,如果 jobs 已经关闭了,并且通道中所有的值都已经接收完毕,那么 more 的值将是 false。当我们完成所有的任务时,将使用这个特性通过 done 通道去进行通知。

    for j := 1; j <= 3; j++ {
        jobs <- j
        fmt.Println("sent job", j)
    }
    close(jobs)
    fmt.Println("sent all jobs")
    //这里使用 jobs 发送 3 个任务到工作函数中,然后关闭 jobs。

    <-done
    //我们使用前面学到的通道同步方法等待任务结束。
}
$ go run closing-channels.go
sent job 1
received job 1
sent job 2
received job 2
sent job 3
received job 3
sent all jobs
received all jobs

通过关闭通道的学习,也让下面学习通道遍历水到渠成。

通道遍历

前面的例子中,我们讲过 for 和 range为基本的数据结构提供了迭代的功能。我们也可以使用这个语法来遍历从通道中取得的值。

package main

import "fmt"

func main() {

    queue := make(chan string, 2)
    queue <- "one"
    queue <- "two"
    close(queue)
    //我们将遍历在 queue 通道中的两个值。

    for elem := range queue {
        fmt.Println(elem)
    }
    //这个 range 迭代从 queue 中得到的每个值。因为我们在前面 close 了这个通道,这个迭代会在接收完 2 个值之后结束。如果我们没有 close 它,我们将在这个循环中继续阻塞执行,等待接收第三个值
}
$ go run range-over-channels.go
one
two

这个例子也让我们看到,一个非空的通道也是可以关闭的,但是通道中剩下的值仍然可以被接收到。

定时器

我们常常需要在后面一个时刻运行 Go 代码,或者在某段时间间隔内重复运行。Go 的内置 定时器 和 打点器 特性让这写很容易实现。我们将先学习定时器,然后再学习打点器

package main

import "time"
import "fmt"

func main() {

    timer1 := time.NewTimer(time.Second * 2)
    //定时器表示在未来某一时刻的独立事件。你告诉定时器需要等待的时间,然后它将提供一个用于通知的通道。这里的定时器将等待 2 秒。

    <-timer1.C
    fmt.Println("Timer 1 expired")
    //<-timer1.C 直到这个定时器的通道 C 明确的发送了定时器失效的值之前,将一直阻塞。

    timer2 := time.NewTimer(time.Second)
    go func() {
        <-timer2.C
        fmt.Println("Timer 2 expired")
    }()
    stop2 := timer2.Stop()
    if stop2 {
        fmt.Println("Timer 2 stopped")
    }
    //如果你需要的仅仅是单纯的等待,你需要使用 time.Sleep。定时器是有用原因之一就是你可以在定时器失效之前,取消这个定时器。这是一个例子
}

第一个定时器将在程序开始后 ~2s 失效,但是第二个在它没失效之前就停止了。

$ go run timers.go
Timer 1 expired
Timer 2 stopped

打点器

定时器 是当你想要在未来某一刻执行一次时使用的 - 打点器 则是当你想要在固定的时间间隔重复执行准备的。这里是一个打点器的例子,它将定时的执行,知道我们将它停止。

package main

import "time"
import "fmt"

func main() {

    ticker := time.NewTicker(time.Millisecond * 500)
    go func() {
        for t := range ticker.C {
            fmt.Println("Tick at", t)
        }
    }()
    //打点器和定时器的机制有点相似:一个通道用来发送数据。这里我们在这个通道上使用内置的 range 来迭代值每隔500ms 发送一次的值。

    time.Sleep(time.Millisecond * 1500)
    ticker.Stop()
    fmt.Println("Ticker stopped")
    //打点器可以和定时器一样被停止。一旦一个打点停止了,将不能再从它的通道中接收到值。我们将在运行后 1500ms停止这个打点器。
}

当我们运行这个程序时,这个打点器会在我们停止它前打点3次。

$ go run tickers.go
Tick at 2012-09-23 11:29:56.487625 -0700 PDT
Tick at 2012-09-23 11:29:56.988063 -0700 PDT
Tick at 2012-09-23 11:29:57.488076 -0700 PDT
Ticker stopped

工作池

在这个例子中,我们将看到如何使用 Go 协程和通道实现一个工作池 

package main

import "fmt"
import "time"

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Println("worker", id, "processing job", j)
        time.Sleep(time.Second)
        results <- j * 2
    }
}
//这是我们将要在多个并发实例中支持的任务了。这些执行者将从 jobs 通道接收任务,并且通过 results 发送对应的结果。我们将让每个任务间隔 1s 来模仿一个耗时的任务。

func main() {

    jobs := make(chan int, 100)
    results := make(chan int, 100)
    //为了使用 worker 工作池并且收集他们的结果,我们需要2 个通道。

    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }
    //这里启动了 3 个 worker,初始是阻塞的,因为还没有传递任务。

    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)
    //这里我们发送 9 个 jobs,然后 close 这些通道来表示这些就是所有的任务了。

    for a := 1; a <= 9; a++ {
        <-results
    }
    //最后,我们收集所有这些人物的返回值。
}

执行这个程序,显示 9 个任务被多个 worker 执行。整个程序处理所有的任务仅执行了 3s 而不是 9s,是因为 3 个 worker是并行的。

$ time go run worker-pools.go 
worker 1 processing job 1
worker 2 processing job 2
worker 3 processing job 3
worker 1 processing job 4
worker 2 processing job 5
worker 3 processing job 6
worker 1 processing job 7
worker 2 processing job 8
worker 3 processing job 9

real	0m3.149s

速率限制

速率限制(英) 是一个重要的控制服务资源利用和质量的途径。Go 通过 Go 协程、通道和打点器优美的支持了速率限制。

package main

import "time"
import "fmt"

func main() {

    requests := make(chan int, 5)
    for i := 1; i <= 5; i++ {
        requests <- i
    }
    close(requests)
    //首先我们将看一下基本的速率限制。假设我们想限制我们接收请求的处理,我们将这些请求发送给一个相同的通道。

    limiter := time.Tick(time.Millisecond * 200)
    //这个 limiter 通道将每 200ms 接收一个值。这个是速率限制任务中的管理器。

    for req := range requests {
        <-limiter
        fmt.Println("request", req, time.Now())
    }
    //通过在每次请求前阻塞 limiter 通道的一个接收,我们限制自己每 200ms 执行一次请求。

    burstyLimiter := make(chan time.Time, 3)
    //有时候我们想临时进行速率限制,并且不影响整体的速率控制我们可以通过通道缓冲来实现。这个 burstyLimiter 通道用来进行 3 次临时的脉冲型速率限制。

    for i := 0; i < 3; i++ {
        burstyLimiter <- time.Now()
    }
    //想将通道填充需要临时改变次的值,做好准备。

    go func() {
        for t := range time.Tick(time.Millisecond * 200) {
            burstyLimiter <- t
        }
    }()
    //每 200 ms 我们将添加一个新的值到 burstyLimiter中,直到达到 3 个的限制。

    burstyRequests := make(chan int, 5)
    for i := 1; i <= 5; i++ {
        burstyRequests <- i
    }
    close(burstyRequests)
    for req := range burstyRequests {
        <-burstyLimiter
        fmt.Println("request", req, time.Now())
    }
    //现在模拟超过 5 个的接入请求。它们中刚开始的 3 个将由于受 burstyLimiter 的“脉冲”影响。
}

运行程序,我们看到第一批请求意料之中的大约每 200ms 处理一次。

$ go run rate-limiting.go
request 1 2012-10-19 00:38:18.687438 +0000 UTC
request 2 2012-10-19 00:38:18.887471 +0000 UTC
request 3 2012-10-19 00:38:19.087238 +0000 UTC
request 4 2012-10-19 00:38:19.287338 +0000 UTC
request 5 2012-10-19 00:38:19.487331 +0000 UTC

request 1 2012-10-19 00:38:20.487578 +0000 UTC
request 2 2012-10-19 00:38:20.487645 +0000 UTC
request 3 2012-10-19 00:38:20.487676 +0000 UTC
request 4 2012-10-19 00:38:20.687483 +0000 UTC
request 5 2012-10-19 00:38:20.887542 +0000 UTC

第二批请求,我们直接连续处理了 3 次,这是由于这个“脉冲”速率控制,然后大约每 200ms 处理其余的 2 个。

原子计数器

Go 中最主要的状态管理方式是通过通道间的沟通来完成的,我们在工作池的例子中碰到过,但是还是有一些其他的方法来管理状态的。这里我们将看看如何使用sync/atomic包在多个 Go 协程中进行 原子计数 

package main

import "fmt"
import "time"
import "sync/atomic"
import "runtime"

func main() {

    var ops uint64 = 0
    //我们将使用一个无符号整形数来表示(永远是正整数)这个计数器。

    for i := 0; i < 50; i++ {
        go func() {
            for {
            //为了模拟并发更新,我们启动 50 个 Go 协程,对计数器每隔 1ms (译者注:应为非准确时间)进行一次加一操作。

                atomic.AddUint64(&ops, 1)
                //使用 AddUint64 来让计数器自动增加,使用& 语法来给出 ops 的内存地址。
                runtime.Gosched()
            }
        }()
    }
    //允许其它 Go 协程的执行

    time.Sleep(time.Second)
    //等待一秒,让 ops 的自加操作执行一会。

    opsFinal := atomic.LoadUint64(&ops)
    fmt.Println("ops:", opsFinal)
    //为了在计数器还在被其它 Go 协程更新时,安全的使用它,我们通过 LoadUint64 将当前值得拷贝提取到 opsFinal中。和上面一样,我们需要给这个函数所取值的内存地址 &ops
}

执行这个程序,显示我们执行了大约 40,000 次操作

$ go run atomic-counters.go
ops: 40200

下面,我们将看一下互斥锁——管理状态的另一个工具

互斥锁

在前面的例子中,我们看到了如何使用原子操作来管理简单的计数器。对于更加复杂的情况,我们可以使用一个互斥锁来在 Go 协程间安全的访问数据。

package main

import (
    "fmt"
    "math/rand"
    "runtime"
    "sync"
    "sync/atomic"
    "time"
)

func main() {

    var state = make(map[int]int)
    //在我们的例子中,state 是一个 map。

    var mutex = &sync.Mutex{}
    //这里的 mutex 将同步对 state 的访问。

    var ops int64 = 0
    //为了比较基于互斥锁的处理方式和我们后面将要看到的其他方式,ops 将记录我们对 state 的操作次数。

    for r := 0; r < 100; r++ {
        go func() {
            total := 0
            for {
            //这里我们运行 100 个 Go 协程来重复读取 state。

                key := rand.Intn(5)
                mutex.Lock()
                total += state[key]
                mutex.Unlock()
                atomic.AddInt64(&ops, 1)
                //每次循环读取,我们使用一个键来进行访问,Lock() 这个 mutex 来确保对 state 的独占访问,读取选定的键的值,Unlock() 这个mutex,并且 ops 值加 1。

                runtime.Gosched()
            }
        }()
    }
    //为了确保这个 Go 协程不会再调度中饿死,我们在每次操作后明确的使用 runtime.Gosched()进行释放。这个释放一般是自动处理的,像例如每个通道操作后或者 time.Sleep 的阻塞调用后相似,但是在这个例子中我们需要手动的处理。

    for w := 0; w < 10; w++ {
        go func() {
            for {
                key := rand.Intn(5)
                val := rand.Intn(100)
                mutex.Lock()
                state[key] = val
                mutex.Unlock()
                atomic.AddInt64(&ops, 1)
                runtime.Gosched()
            }
        }()
    }
    //同样的,我们运行 10 个 Go 协程来模拟写入操作,使用和读取相同的模式。

    time.Sleep(time.Second)
    //让这 10 个 Go 协程对 state 和 mutex 的操作运行 1 s。

    opsFinal := atomic.LoadInt64(&ops)
    fmt.Println("ops:", opsFinal)
    //获取并输出最终的操作计数。

    mutex.Lock()
    fmt.Println("state:", state)
    mutex.Unlock()
    //对 state 使用一个最终的锁,显示它是如何结束的。
}

运行这个程序,显示我们对已进行了同步的 state 执行了3,500,000 次操作。

$ go run mutexes.go
ops: 3598302
state: map[1:38 4:98 2:23 3:85 0:44]

接下来我们将看一下只使用 Go 协程和通道是如何实现相同的状态控制任务的。

Go 状态协程

在前面的例子中,我们用互斥锁进行了明确的锁定来让共享的state 跨多个 Go 协程同步访问。另一个选择是使用内置的 Go协程和通道的的同步特性来达到同样的效果。这个基于通道的方法和 Go 通过通信以及 每个 Go 协程间通过通讯来共享内存,确保每块数据有单独的 Go 协程所有的思路是一致的。

package main

import (
    "fmt"
    "math/rand"
    "sync/atomic"
    "time"
)

type readOp struct {
    key  int
    resp chan int
}
type writeOp struct {
    key  int
    val  int
    resp chan bool
}
//在这个例子中,state 将被一个单独的 Go 协程拥有。这就能够保证数据在并行读取时不会混乱。为了对 state 进行读取或者写入,其他的 Go 协程将发送一条数据到拥有的 Go协程中,然后接收对应的回复。结构体 readOp 和 writeOp封装这些请求,并且是拥有 Go 协程响应的一个方式。

func main() {

    var ops int64
    //和前面一样,我们将计算我们执行操作的次数。

    reads := make(chan *readOp)
    writes := make(chan *writeOp)
    //reads 和 writes 通道分别将被其他 Go 协程用来发布读和写请求。

    go func() {
        var state = make(map[int]int)
        for {
            select {
            case read := <-reads:
                read.resp <- state[read.key]
            case write := <-writes:
                state[write.key] = write.val
                write.resp <- true
            }
        }
    }()
    //这个就是拥有 state 的那个 Go 协程,和前面例子中的map一样,不过这里是被这个状态协程私有的。这个 Go 协程反复响应到达的请求。先响应到达的请求,然后返回一个值到响应通道 resp 来表示操作成功(或者是 reads 中请求的值)

    for r := 0; r < 100; r++ {
        go func() {
            for {
                read := &readOp{
                    key:  rand.Intn(5),
                    resp: make(chan int)}
                reads <- read
                <-read.resp
                atomic.AddInt64(&ops, 1)
            }
        }()
    }
    //启动 100 个 Go 协程通过 reads 通道发起对 state 所有者Go 协程的读取请求。每个读取请求需要构造一个 readOp,发送它到 reads 通道中,并通过给定的 resp 通道接收结果。

    for w := 0; w < 10; w++ {
        go func() {
            for {
                write := &writeOp{
                    key:  rand.Intn(5),
                    val:  rand.Intn(100),
                    resp: make(chan bool)}
                writes <- write
                <-write.resp
                atomic.AddInt64(&ops, 1)
            }
        }()
    }
    //用相同的方法启动 10 个写操作。

    time.Sleep(time.Second)
    //让 Go 协程们跑 1s。

    opsFinal := atomic.LoadInt64(&ops)
    fmt.Println("ops:", opsFinal)
    //最后,获取并报告 ops 值。
}

运行这个程序显示这个基于 Go 协程的转台管理的例子达到了每秒大约 800,000 次操作。

$ go run stateful-goroutines.go
ops: 807434

在这个特殊的例子中,基于 Go 协程的比基于互斥锁的稍复杂。这在某些例子中会有用,例如,在你有其他通道包含其中或者当你管理多个这样的互斥锁容易出错的时候。你应该使用最自然的方法,特别是关于程序正确性的时候。

排序

Go 的 sort 包实现了内置和用户自定义数据类型的排序功能。我们首先关注内置数据类型的排序。

package main

import "fmt"
import "sort"

func main() {

    strs := []string{"c", "a", "b"}
    sort.Strings(strs)
    fmt.Println("Strings:", strs)
    //排序方法是正对内置数据类型的;这里是一个字符串的例子。注意排序是原地更新的,所以他会改变给定的序列并且不返回一个新值。

    ints := []int{7, 2, 4}
    sort.Ints(ints)
    fmt.Println("Ints:   ", ints)
    //一个 int 排序的例子。

    s := sort.IntsAreSorted(ints)
    fmt.Println("Sorted: ", s)
    //我们也可以使用 sort 来检查一个序列是不是已经是排好序的。
}

运行程序,打印排序好的字符串和整形序列以及我们AreSorted测试的结构 true

$ go run sorting.go
Strings: [a b c]
Ints:    [2 4 7]
Sorted:  true

使用函数自定义排序

有时候我们想使用和集合的自然排序不同的方法对集合进行排序。例如,我们想按照字母的长度而不是首字母顺序对字符串排序。这里是一个 Go 自定义排序的例子。

package main

import "sort"
import "fmt"

type ByLength []string
//为了在 Go 中使用自定义函数进行排序,我们需要一个对应的类型。这里我们创建一个为内置 []string 类型的别名的ByLength 类型

func (s ByLength) Len() int {
    return len(s)
}
func (s ByLength) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}
func (s ByLength) Less(i, j int) bool {
    return len(s[i]) < len(s[j])
}
//我们在类型中实现了 sort.Interface 的 Len,Less和 Swap 方法,这样我们就可以使用 sort 包的通用Sort 方法了,Len 和 Swap 通常在各个类型中都差不多,Less 将控制实际的自定义排序逻辑。在我们的例子中,我们想按字符串长度增加的顺序来排序,所以这里使用了 len(s[i]) 和 len(s[j])。

func main() {
    fruits := []string{"peach", "banana", "kiwi"}
    sort.Sort(ByLength(fruits))
    fmt.Println(fruits)
    //一切都准备好了,我们现在可以通过将原始的 fruits 切片转型成 ByLength 来实现我们的自定排序了。然后对这个转型的切片使用 sort.Sort 方法。
}

运行这个程序,和预期的一样,显示了一个按照字符串长度排序的列表。

$ go run sorting-by-functions.go 
[kiwi peach banana]

类似的,参照这个创建一个自定义类型的方法,实现这个类型的这三个接口方法,然后在一个这个自定义类型的集合上调用 sort.Sort 方法,我们就可以使用任意的函数来排序 Go 切片了。

Panic

panic 意味着有些出乎意料的错误发生。通常我们用它来表示程序正常运行中不应该出现的,后者我么没有处理好的错误。

package main

import "os"

func main() {

    panic("a problem")
    //我们将在真个网站中使用 panic 来检查预期外的错误。这个是唯一一个为 panic 准备的例子。

    _, err := os.Create("/tmp/file")
    if err != nil {
        panic(err)
    }
    //panic 的一个基本用法就是在一个函数返回了错误值但是我们并不知道(或者不想)处理时终止运行。这里是一个在创建一个新文件时返回异常错误时的panic 用法。
}

运行程序将会引起 panic,输出一个错误消息和 Go 运行时栈信息,并且返回一个非零的状态码。

$ go run panic.go
panic: a problem

goroutine 1 [running]:
main.main()
	/.../panic.go:12 +0x47
...
exit status 2

注意,不像有些语言中处理多个错误那样,在 Go 中习惯使用错误码返回任意可能的值。

Defer

Defer 被用来确保一个函数调用在程序执行结束前执行。同样用来执行一些清理工作。 defer 用在像其他语言中的ensure 和 finally用到的地方。

package main

import "fmt"
import "os"

func main() {
    //假设我们想要创建一个文件,向它进行写操作,然后在结束时关闭它。这里展示了如何通过 defer 来做到这一切。

    f := createFile("/tmp/defer.txt")
    defer closeFile(f)
    writeFile(f)
}
//在 closeFile 后得到一个文件对象,我们使用 defer通过 closeFile 来关闭这个文件。这会在封闭函数(main)结束时执行,就是 writeFile 结束后。

func createFile(p string) *os.File {
    fmt.Println("creating")
    f, err := os.Create(p)
    if err != nil {
        panic(err)
    }
    return f
}

 

func writeFile(f *os.File) {
    fmt.Println("writing")
    fmt.Fprintln(f, "data")

 

}

 

func closeFile(f *os.File) {
    fmt.Println("closing")
    f.Close()
}

执行程序,确认这个文件在写入后是已关闭的。

$ go run defer.go
creating
writing
closing

组合函数

我们经常需要程序在数据集上执行操作,比如选择满足给定条件的所有项,或者将所有的项通过一个自定义函数映射到一个新的集合上。

在某些语言中,会习惯使用泛型。Go 不支持泛型,在 Go 中,当你的程序或者数据类型需要时,通常是通过组合的方式来提供操作函数。

这是一些 strings 切片的组合函数示例。你可以使用这些例子来构建自己的函数。注意有时候,直接使用内联组合操作代码会更清晰,而不是创建并调用一个帮助函数。

package main

import "strings"
import "fmt"
//返回目标字符串 t 出现的的第一个索引位置,或者在没有匹配值时返回 -1。

func Index(vs []string, t string) int {
    for i, v := range vs {
        if v == t {
            return i
        }
    }
    return -1
}

func Include(vs []string, t string) bool {
    return Index(vs, t) >= 0
}
//如果目标字符串 t 在这个切片中则返回 true。

func Any(vs []string, f func(string) bool) bool {
    for _, v := range vs {
        if f(v) {
            return true
        }
    }
    return false
}
//如果这些切片中的字符串有一个满足条件 f 则返回true。

func All(vs []string, f func(string) bool) bool {
    for _, v := range vs {
        if !f(v) {
            return false
        }
    }
    return true
}
//如果切片中的所有字符串都满足条件 f 则返回 true。

func Filter(vs []string, f func(string) bool) []string {
    vsf := make([]string, 0)
    for _, v := range vs {
        if f(v) {
            vsf = append(vsf, v)
        }
    }
    return vsf
}
//返回一个包含所有切片中满足条件 f 的字符串的新切片。

func Map(vs []string, f func(string) string) []string {
    vsm := make([]string, len(vs))
    for i, v := range vs {
        vsm[i] = f(v)
    }
    return vsm
}
//返回一个对原始切片中所有字符串执行函数 f 后的新切片。

func main() {

    var strs = []string{"peach", "apple", "pear", "plum"}
    //这里试试这些组合函数。

    fmt.Println(Index(strs, "pear"))

    fmt.Println(Include(strs, "grape"))

    fmt.Println(Any(strs, func(v string) bool {
        return strings.HasPrefix(v, "p")
    }))

    fmt.Println(All(strs, func(v string) bool {
        return strings.HasPrefix(v, "p")
    }))

    fmt.Println(Filter(strs, func(v string) bool {
        return strings.Contains(v, "e")
    }))

    fmt.Println(Map(strs, strings.ToUpper))
//上面的例子都是用的匿名函数,但是你也可以使用类型正确的命名函数
}
$ go run collection-functions.go 
2
false
true
false
[peach apple pear]
[PEACH APPLE PEAR PLUM]

字符串函数

标准库的 strings 包提供了很多有用的字符串相关的函数。这里是一些用来让你对这个包有个初步了解的例子。

package main

import s "strings"
import "fmt"

var p = fmt.Println
//我们给 fmt.Println 一个短名字的别名,我们随后将会经常用到。

func main() {

    p("Contains:  ", s.Contains("test", "es"))
    p("Count:     ", s.Count("test", "t"))
    p("HasPrefix: ", s.HasPrefix("test", "te"))
    p("HasSuffix: ", s.HasSuffix("test", "st"))
    p("Index:     ", s.Index("test", "e"))
    p("Join:      ", s.Join([]string{"a", "b"}, "-"))
    p("Repeat:    ", s.Repeat("a", 5))
    p("Replace:   ", s.Replace("foo", "o", "0", -1))
    p("Replace:   ", s.Replace("foo", "o", "0", 1))
    p("Split:     ", s.Split("a-b-c-d-e", "-"))
    p("ToLower:   ", s.ToLower("TEST"))
    p("ToUpper:   ", s.ToUpper("test"))
    p()
    //这是一些 strings 中的函数例子。注意他们都是包中的函数,不是字符串对象自身的方法,这意味着我们需要考虑在调用时传递字符作为第一个参数进行传递。
    //你可以在 strings包文档中找到更多的函数

    p("Len: ", len("hello"))
    p("Char:", "hello"[1])
    //虽然不是 strings 的一部分,但是仍然值得一提的是获取字符串长度和通过索引获取一个字符的机制。
}
$ go run string-functions.go
Contains:   true
Count:      2
HasPrefix:  true
HasSuffix:  true
Index:      1
Join:       a-b
Repeat:     aaaaa
Replace:    f00
Replace:    f0o
Split:      [a b c d e]
toLower:    test
ToUpper:    TEST

Len:  5
Char: 101

字符串格式化

Go 在传统的printf 中对字符串格式化提供了优异的支持。这里是一些基本的字符串格式化的人物的例子。

package main

import "fmt"
import "os"

type point struct {
    x, y int
}

func main() {

    p := point{1, 2}
    fmt.Printf("%v\n", p)
    //Go 为常规 Go 值的格式化设计提供了多种打印方式。例如,这里打印了 point 结构体的一个实例。

    fmt.Printf("%+v\n", p)
    //如果值是一个结构体,%+v 的格式化输出内容将包括结构体的字段名。

    fmt.Printf("%#v\n", p)
    //%#v 形式则输出这个值的 Go 语法表示。例如,值的运行源代码片段。

    fmt.Printf("%T\n", p)
    //需要打印值的类型,使用 %T。

    fmt.Printf("%t\n", true)
    //格式化布尔值是简单的。

    fmt.Printf("%d\n", 123)
    //格式化整形数有多种方式,使用 %d进行标准的十进制格式化。

    fmt.Printf("%b\n", 14)
    //这个输出二进制表示形式。

    fmt.Printf("%c\n", 33)
    //这个输出给定整数的对应字符。

    fmt.Printf("%x\n", 456)
    //%x 提供十六进制编码。

    fmt.Printf("%f\n", 78.9)
    //对于浮点型同样有很多的格式化选项。使用 %f 进行最基本的十进制格式化。

    fmt.Printf("%e\n", 123400000.0)
    fmt.Printf("%E\n", 123400000.0)
    //%e 和 %E 将浮点型格式化为(稍微有一点不同的)科学技科学记数法表示形式。

    fmt.Printf("%s\n", "\"string\"")
    //使用 %s 进行基本的字符串输出。

    fmt.Printf("%q\n", "\"string\"")
    //像 Go 源代码中那样带有双引号的输出,使用 %q。

    fmt.Printf("%x\n", "hex this")
    //和上面的整形数一样,%x 输出使用 base-16 编码的字符串,每个字节使用 2 个字符表示。

    fmt.Printf("%p\n", &p)
    //要输出一个指针的值,使用 %p。

    fmt.Printf("|%6d|%6d|\n", 12, 345)
    //当输出数字的时候,你将经常想要控制输出结果的宽度和精度,可以使用在 % 后面使用数字来控制输出宽度。默认结果使用右对齐并且通过空格来填充空白部分。

    fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)
    //你也可以指定浮点型的输出宽度,同时也可以通过 宽度.精度 的语法来指定输出的精度。

    fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45)
    //要最对齐,使用 - 标志。

    fmt.Printf("|%6s|%6s|\n", "foo", "b")
    //你也许也想控制字符串输出时的宽度,特别是要确保他们在类表格输出时的对齐。这是基本的右对齐宽度表示。

    fmt.Printf("|%-6s|%-6s|\n", "foo", "b")
    //要左对齐,和数字一样,使用 - 标志。

    s := fmt.Sprintf("a %s", "string")
    fmt.Println(s)
    //到目前为止,我们已经看过 Printf了,它通过 os.Stdout输出格式化的字符串。Sprintf 则格式化并返回一个字符串而不带任何输出。

    fmt.Fprintf(os.Stderr, "an %s\n", "error")
    //你可以使用 Fprintf 来格式化并输出到 io.Writers而不是 os.Stdout。
}
$ go run string-formatting.go
{1 2}
{x:1 y:2}
main.point{x:1, y:2}
main.point
true
123
1110
!
1c8
78.900000
1.234000e+08
1.234000E+08
"string"
"\"string\""
6865782074686973
0x42135100
|    12|   345|
|  1.20|  3.45|
|1.20  |3.45  |
|   foo|     b|
|foo   |b     |
a string
an error

正则表达式

Go 提供内置的正则表达式。这里是 Go 中基本的正则相关功能的例子。

package main

import "bytes"
import "fmt"
import "regexp"

func main() {

    match, _ := regexp.MatchString("p([a-z]+)ch", "peach")
    fmt.Println(match)
    //这个测试一个字符串是否符合一个表达式。

    r, _ := regexp.Compile("p([a-z]+)ch")
    //上面我们是直接使用字符串,但是对于一些其他的正则任务,你需要使用 Compile 一个优化的 Regexp 结构体。

    fmt.Println(r.MatchString("peach"))
    //这个结构体有很多方法。这里是类似我们前面看到的一个匹配测试。

    fmt.Println(r.FindString("peach punch"))
    //这是查找匹配字符串的。

    fmt.Println(r.FindStringIndex("peach punch"))
    //这个也是查找第一次匹配的字符串的,但是返回的匹配开始和结束位置索引,而不是匹配的内容。

    fmt.Println(r.FindStringSubmatch("peach punch"))
    //Submatch 返回完全匹配和局部匹配的字符串。例如,这里会返回 p([a-z]+)ch 和 `([a-z]+) 的信息。

    fmt.Println(r.FindStringSubmatchIndex("peach punch"))
    //类似的,这个会返回完全匹配和局部匹配的索引位置。

    fmt.Println(r.FindAllString("peach punch pinch", -1))
    //带 All 的这个函数返回所有的匹配项,而不仅仅是首次匹配项。例如查找匹配表达式的所有项。

    fmt.Println(r.FindAllStringSubmatchIndex(
        "peach punch pinch", -1))
    //All 同样可以对应到上面的所有函数。

    fmt.Println(r.FindAllString("peach punch pinch", 2))
    //这个函数提供一个正整数来限制匹配次数。

    fmt.Println(r.Match([]byte("peach")))
    //上面的例子中,我们使用了字符串作为参数,并使用了如 MatchString 这样的方法。我们也可以提供 []byte参数并将 String 从函数命中去掉。

    r = regexp.MustCompile("p([a-z]+)ch")
    fmt.Println(r)
    //创建正则表示式常量时,可以使用 Compile 的变体MustCompile 。因为 Compile 返回两个值,不能用语常量。

    fmt.Println(r.ReplaceAllString("a peach", "<fruit>"))
    //regexp 包也可以用来替换部分字符串为其他值。

    in := []byte("a peach")
    out := r.ReplaceAllFunc(in, bytes.ToUpper)
    fmt.Println(string(out))
    //Func 变量允许传递匹配内容到一个给定的函数中
}
$ go run regular-expressions.go 
true
true
peach
[0 5]
[peach ea]
[0 5 1 3]
[peach punch pinch]
[[0 5 1 3] [6 11 7 9] [12 17 13 15]]
[peach punch]
true
p([a-z]+)ch
a <fruit>
a PEACH

完整的 Go 正则表达式参考,请查阅 regexp 包文档。

JSON

Go 提供内置的 JSON 编解码支持,包括内置或者自定义类型与 JSON 数据之间的转化。

package main

import "encoding/json"
import "fmt"
import "os"

//下面我们将使用这两个结构体来演示自定义类型的编码和解码。
type Response1 struct {
    Page   int
    Fruits []string
}
type Response2 struct {
    Page   int      `json:"page"`
    Fruits []string `json:"fruits"`
}

func main() {

    //首先我们来看一下基本数据类型到 JSON 字符串的编码过程。这里是一些原子值的例子。
    bolB, _ := json.Marshal(true)
    fmt.Println(string(bolB))

    intB, _ := json.Marshal(1)
    fmt.Println(string(intB))

    fltB, _ := json.Marshal(2.34)
    fmt.Println(string(fltB))

    strB, _ := json.Marshal("gopher")
    fmt.Println(string(strB))

    slcD := []string{"apple", "peach", "pear"}
    slcB, _ := json.Marshal(slcD)
    fmt.Println(string(slcB))
    //这里是一些切片和 map 编码成 JSON 数组和对象的例子。

    mapD := map[string]int{"apple": 5, "lettuce": 7}
    mapB, _ := json.Marshal(mapD)
    fmt.Println(string(mapB))

    res1D := &Response1{
        Page:   1,
        Fruits: []string{"apple", "peach", "pear"}}
    res1B, _ := json.Marshal(res1D)
    fmt.Println(string(res1B))
    //JSON 包可以自动的编码你的自定义类型。编码仅输出可导出的字段,并且默认使用他们的名字作为 JSON 数据的键。

    res2D := &Response2{
        Page:   1,
        Fruits: []string{"apple", "peach", "pear"}}
    res2B, _ := json.Marshal(res2D)
    fmt.Println(string(res2B))
    //你可以给结构字段声明标签来自定义编码的 JSON 数据键名称。在上面 Response2 的定义可以作为这个标签这个的一个例子。

    byt := []byte(`{"num":6.13,"strs":["a","b"]}`)
    //现在来看看解码 JSON 数据为 Go 值的过程。这里是一个普通数据结构的解码例子。

    var dat map[string]interface{}
//我们需要提供一个 JSON 包可以存放解码数据的变量。这里的 map[string]interface{} 将保存一个 string 为键,值为任意值的map。

    if err := json.Unmarshal(byt, &dat); err != nil {
        panic(err)
    }
    fmt.Println(dat)
//这里就是实际的解码和相关的错误检查。

    num := dat["num"].(float64)
    fmt.Println(num)
    //为了使用解码 map 中的值,我们需要将他们进行适当的类型转换。例如这里我们将 num 的值转换成 float64类型。

    strs := dat["strs"].([]interface{})
    str1 := strs[0].(string)
    fmt.Println(str1)
    //访问嵌套的值需要一系列的转化。

    str := `{"page": 1, "fruits": ["apple", "peach"]}`
    res := &Response2{}
    json.Unmarshal([]byte(str), &res)
    fmt.Println(res)
    fmt.Println(res.Fruits[0])
    //我们也可以解码 JSON 值到自定义类型。这个功能的好处就是可以为我们的程序带来额外的类型安全加强,并且消除在访问数据时的类型断言。

    enc := json.NewEncoder(os.Stdout)
    d := map[string]int{"apple": 5, "lettuce": 7}
    enc.Encode(d)
    //在上面的例子中,我们经常使用 byte 和 string 作为使用标准输出时数据和 JSON 表示之间的中间值。我们也可以和os.Stdout 一样,直接将 JSON 编码直接输出至 os.Writer流中,或者作为 HTTP 响应体。
}
$ go run json.go
true
1
2.34
"gopher"
["apple","peach","pear"]
{"apple":5,"lettuce":7}
{"Page":1,"Fruits":["apple","peach","pear"]}
{"page":1,"fruits":["apple","peach","pear"]}
map[num:6.13 strs:[a b]]
6.13
a
&{1 [apple peach]}
apple
{"apple":5,"lettuce":7}

这里我们已经覆盖了基本的 Go JSON 知识,但是查阅JSON 和 Go博文和 JSON 包文档 来获取更多的信息。

 

时间

Go 对时间和时间段提供了大量的支持;这里是一些例子。

package main

import "fmt"
import "time"

func main() {
    p := fmt.Println

    now := time.Now()
    p(now)
    //得到当前时间。

    then := time.Date(
        2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
    p(then)
/span>
    //通过提供年月日等信息,你可以构建一个 time。时间总是关联着位置信息,例如时区。

    p(then.Year())
    p(then.Month())
    p(then.Day())
    p(then.Hour())
    p(then.Minute())
    p(then.Second())
    p(then.Nanosecond())
    p(then.Location())
    //你可以提取出时间的各个组成部分。

    p(then.Weekday())
    //输出是星期一到日的 Weekday 也是支持的。

    p(then.Before(now))
    p(then.After(now))
    p(then.Equal(now))
    //这些方法来比较两个时间,分别测试一下是否是之前,之后或者是同一时刻,精确到秒。

    diff := now.Sub(then)
    p(diff)
    //方法 Sub 返回一个 Duration 来表示两个时间点的间隔时间。

    p(diff.Hours())
    p(diff.Minutes())
    p(diff.Seconds())
    p(diff.Nanoseconds())
    //我们计算出不同单位下的时间长度值。

    p(then.Add(diff))
    p(then.Add(-diff))
    //你可以使用 Add 将时间后移一个时间间隔,或者使用一个 - 来将时间前移一个时间间隔。
}
$ go run time.go
2012-10-31 15:50:13.793654 +0000 UTC
2009-11-17 20:34:58.651387237 +0000 UTC
2009
November
17
20
34
58
651387237
UTC
Tuesday
true
false
false
25891h15m15.142266763s
25891.25420618521
1.5534752523711128e+06
9.320851514226677e+07
93208515142266763
2012-10-31 15:50:13.793654 +0000 UTC
2006-12-05 01:19:43.509120474 +0000 UTC

下面我们将看到时间到 Unix 时间的相关概念。

时间戳

一般程序会有获取 Unix 时间的秒数,毫秒数,或者微秒数的需要。来看看如何用 Go 来实现。

package main

import "fmt"
import "time"

func main() {

    now := time.Now()
    secs := now.Unix()
    nanos := now.UnixNano()
    fmt.Println(now)
    //分别使用带 Unix 或者 UnixNano 的 time.Now来获取从自协调世界时起到现在的秒数或者纳秒数。

    millis := nanos / 1000000
    fmt.Println(secs)
    fmt.Println(millis)
    fmt.Println(nanos)
    //注意 UnixMillis 是不存在的,所以要得到毫秒数的话,你要自己手动的从纳秒转化一下。

    fmt.Println(time.Unix(secs, 0))
    fmt.Println(time.Unix(0, nanos))
    //你也可以将协调世界时起的整数秒或者纳秒转化到相应的时间。
}
$ go run epoch.go 
2012-10-31 16:13:58.292387 +0000 UTC
1351700038
1351700038292
1351700038292387000
2012-10-31 16:13:58 +0000 UTC
2012-10-31 16:13:58.292387 +0000 UTC

下面我们将看看另一个事件相关的任务:时间格式化和解析。

时间的格式化和解析

Go 支持通过基于描述模板的时间格式化和解析。

package main

import "fmt"
import "time"

func main() {
    p := fmt.Println

    t := time.Now()
    p(t.Format(time.RFC3339))
    //这里是一个基本的按照 RFC3339 进行格式化的例子,使用对应模式常量。

    t1, e := time.Parse(
        time.RFC3339,
        "2012-11-01T22:08:41+00:00")
    p(t1)
    //时间解析使用同 Format 相同的形式值。

    p(t.Format("3:04PM"))
    p(t.Format("Mon Jan _2 15:04:05 2006"))
    p(t.Format("2006-01-02T15:04:05.999999-07:00"))
    form := "3 04 PM"
    t2, e := time.Parse(form, "8 41 PM")
    p(t2)
    //Format 和 Parse 使用基于例子的形式来决定日期格式,一般你只要使用 time 包中提供的模式常量就行了,但是你也可以实现自定义模式。模式必须使用时间 Mon Jan 2 15:04:05 MST 2006来指定给定时间/字符串的格式化/解析方式。时间一定要按照如下所示:2006为年,15 为小时,Monday 代表星期几,等等。

    fmt.Printf("%d-%02d-%02dT%02d:%02d:%02d-00:00\n",
        t.Year(), t.Month(), t.Day(),
        t.Hour(), t.Minute(), t.Second())
    //对于纯数字表示的时间,你也可以使用标准的格式化字符串来提出出时间值得组成。

    ansic := "Mon Jan _2 15:04:05 2006"
    _, e = time.Parse(ansic, "8:41PM")
    p(e)
    //Parse 函数在输入的时间格式不正确是会返回一个错误。
}
$ go run time-formatting-parsing.go 
2014-04-15T18:00:15-07:00
2012-11-01 22:08:41 +0000 +0000
6:00PM
Tue Apr 15 18:00:15 2014
2014-04-15T18:00:15.161182-07:00
0000-01-01 20:41:00 +0000 UTC
2014-04-15T18:00:15-00:00
parsing time "8:41PM" as "Mon Jan _2 15:04:05 2006": ...

随机数

Go 的 math/rand 包提供了伪随机数生成器(英)

package main

import "fmt"
import "math/rand"

func main() {

    fmt.Print(rand.Intn(100), ",")
    fmt.Print(rand.Intn(100))
    fmt.Println()
    //例如,rand.Intn 返回一个随机的整数 n,0 <= n <= 100。

    fmt.Println(rand.Float64())
    //rand.Float64 返回一个64位浮点数 f,0.0 <= f <= 1.0。

    fmt.Print((rand.Float64()*5)+5, ",")
    fmt.Print((rand.Float64() * 5) + 5)
    fmt.Println()
    //这个技巧可以用来生成其他范围的随机浮点数,例如5.0 <= f <= 10.0

    s1 := rand.NewSource(42)
    r1 := rand.New(s1)
    //要让伪随机数生成器有确定性,可以给它一个明确的种子。

    fmt.Print(r1.Intn(100), ",")
    fmt.Print(r1.Intn(100))
    fmt.Println()
    //调用上面返回的 rand.Source 的函数和调用 rand 包中函数是相同的。

    s2 := rand.NewSource(42)
    r2 := rand.New(s2)
    fmt.Print(r2.Intn(100), ",")
    fmt.Print(r2.Intn(100))
    fmt.Println()
    //如果使用相同的种子生成的随机数生成器,将会产生相同的随机数序列。
}
$ go run random-numbers.go 
81,87
0.6645600532184904
7.123187485356329,8.434115364335547
5,87
5,87

参阅 math/rand 包文档,提供了 Go 可以提供的其他随量的参考信息。

数字解析

从字符串中解析数字在很多程序中是一个基础常见的任务,在Go 中是这样处理的。

package main

import "strconv"
import "fmt"
//内置的 strconv 包提供了数字解析功能。

func main() {

    f, _ := strconv.ParseFloat("1.234", 64)
    fmt.Println(f)
    //使用 ParseFloat 解析浮点数,这里的 64 表示表示解析的数的位数。

    i, _ := strconv.ParseInt("123", 0, 64)
    fmt.Println(i)
    //在使用 ParseInt 解析整形数时,例子中的参数 0 表示自动推断字符串所表示的数字的进制。64 表示返回的整形数是以 64 位存储的。

    d, _ := strconv.ParseInt("0x1c8", 0, 64)
    fmt.Println(d)
    //ParseInt 会自动识别出十六进制数。

    u, _ := strconv.ParseUint("789", 0, 64)
    fmt.Println(u)
    //ParseUint 也是可用的。

    k, _ := strconv.Atoi("135")
    fmt.Println(k)
    //Atoi 是一个基础的 10 进制整型数转换函数。

    _, e := strconv.Atoi("wat")
    fmt.Println(e)
    //在输入错误时,解析函数会返回一个错误。
}
$ go run number-parsing.go 
1.234
123
456
789
135
strconv.ParseInt: parsing "wat": invalid syntax

下面我们将了解一下另一个常见的解析任务:URL 解析。

URL解析

URL 提供了一个统一资源定位方式。这里了解一下 Go 中是如何解析 URL 的。

package main

import "fmt"
import "net/url"
import "strings"

func main() {

    s := "postgres://user:pass@host.com:5432/path?k=v#f"
    //我们将解析这个 URL 示例,它包含了一个 scheme,认证信息,主机名,端口,路径,查询参数和片段。

    u, err := url.Parse(s)
    if err != nil {
        panic(err)
    }
    //解析这个 URL 并确保解析没有出错。

    fmt.Println(u.Scheme)
    //直接访问 scheme。

    fmt.Println(u.User)
    fmt.Println(u.User.Username())
    p, _ := u.User.Password()
    fmt.Println(p)
    //User 包含了所有的认证信息,这里调用 Username和 Password 来获取独立值。

    fmt.Println(u.Host)
    h := strings.Split(u.Host, ":")
    fmt.Println(h[0])
    fmt.Println(h[1])
    //Host 同时包括主机名和端口信息,如过端口存在的话,使用 strings.Split() 从 Host 中手动提取端口。

    fmt.Println(u.Path)
    fmt.Println(u.Fragment)
    //这里我们提出路径和查询片段信息。

    fmt.Println(u.RawQuery)
    m, _ := url.ParseQuery(u.RawQuery)
    fmt.Println(m)
    fmt.Println(m["k"][0])
    //要得到字符串中的 k=v 这种格式的查询参数,可以使用 RawQuery 函数。你也可以将查询参数解析为一个map。已解析的查询参数 map 以查询字符串为键,对应值字符串切片为值,所以如何只想得到一个键对应的第一个值,将索引位置设置为 [0] 就行了。
}

运行我们的 URL 解析程序,显示全部我们提取的 URL 的不同数据块。

$ go run url-parsing.go 
postgres
user:pass
user
pass
host.com:5432
host.com
5432
/path
f
k=v
map[k:[v]]
v

SHA1散列

SHA1 散列经常用生成二进制文件或者文本块的短标识。例如,git 版本控制系统大量的使用 SHA1 来标识受版本控制的文件和目录。这里是 Go中如何进行 SHA1 散列计算的例子。

package main

import "crypto/sha1"
import "fmt"
//Go 在多个 crypto/* 包中实现了一系列散列函数。

func main() {
    s := "sha1 this string"

    h := sha1.New()
    //产生一个散列值得方式是 sha1.New(),sha1.Write(bytes),然后 sha1.Sum([]byte{})。这里我们从一个新的散列开始。

    h.Write([]byte(s))
    //写入要处理的字节。如果是一个字符串,需要使用[]byte(s) 来强制转换成字节数组。

    bs := h.Sum(nil)
    //这个用来得到最终的散列值的字符切片。Sum 的参数可以用来都现有的字符切片追加额外的字节切片:一般不需要要。

    fmt.Println(s)
    fmt.Printf("%x\n", bs)
    //SHA1 值经常以 16 进制输出,例如在 git commit 中。使用%x 来将散列结果格式化为 16 进制字符串。
}

运行程序计算散列值并以可读 16 进制格式输出。

$ go run sha1-hashes.go
sha1 this string
cf23df2207d99a74fbe169e3eba035e633b65d94

你可以使用和上面相似的方式来计算其他形式的散列值。例如,计算 MD5 散列,引入 crypto/md5 并使用md5.New()方法。

注意,如果你需要密码学上的安全散列,你需要小心的研究一下哈希强度

Base64编码

Go 提供内建的 base64 编解码支持。

package main

import b64 "encoding/base64"
import "fmt"
//这个语法引入了 encoding/base64 包并使用名称 b64代替默认的 base64。这样可以节省点空间。

func main() {

    data := "abc123!?$*&()'-=@~"
//这是将要编解码的字符串。

    sEnc := b64.StdEncoding.EncodeToString([]byte(data))
    fmt.Println(sEnc)
    //Go 同时支持标准的和 URL 兼容的 base64 格式。编码需要使用 []byte 类型的参数,所以要将字符串转成此类型。

    sDec, _ := b64.StdEncoding.DecodeString(sEnc)
    fmt.Println(string(sDec))
    fmt.Println()
    //解码可能会返回错误,如果不确定输入信息格式是否正确,那么,你就需要进行错误检查了。

    uEnc := b64.URLEncoding.EncodeToString([]byte(data))
    fmt.Println(uEnc)
    uDec, _ := b64.URLEncoding.DecodeString(uEnc)
    fmt.Println(string(uDec))
    //使用 URL 兼容的 base64 格式进行编解码。
}

标准 base64 编码和 URL 兼容 base64 编码的编码字符串存在稍许不同(后缀为 + 和 -),但是两者都可以正确解码为原始字符串。

$ go run base64-encoding.go
YWJjMTIzIT8kKiYoKSctPUB+
abc123!?$*&()'-=@~

YWJjMTIzIT8kKiYoKSctPUB-
abc123!?$*&()'-=@~

读文件

读写文件在很多程序中都是必须的基本任务。首先我们看看一些读文件的例子。

package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "os"
)

func check(e error) {
    if e != nil {
        panic(e)
    }
}
//读取文件需要经常进行错误检查,这个帮助方法可以精简下面的错误检查过程。

func main() {

    dat, err := ioutil.ReadFile("/tmp/dat")
    check(err)
    fmt.Print(string(dat))
    //也许大部分基本的文件读取任务是将文件内容读取到内存中。

    f, err := os.Open("/tmp/dat")
    check(err)
    //你经常会想对于一个文件是怎么读并且读取到哪一部分进行更多的控制。对于这个任务,从使用 os.Open打开一个文件获取一个 os.File 值开始。

    b1 := make([]byte, 5)
    n1, err := f.Read(b1)
    check(err)
    fmt.Printf("%d bytes: %s\n", n1, string(b1))
    //从文件开始位置读取一些字节。这里最多读取 5 个字节,并且这也是我们实际读取的字节数。

    o2, err := f.Seek(6, 0)
    check(err)
    b2 := make([]byte, 2)
    n2, err := f.Read(b2)
    check(err)
    fmt.Printf("%d bytes @ %d: %s\n", n2, o2, string(b2))
    //你也可以 Seek 到一个文件中已知的位置并从这个位置开始进行读取。

    o3, err := f.Seek(6, 0)
    check(err)
    b3 := make([]byte, 2)
    n3, err := io.ReadAtLeast(f, b3, 2)
    check(err)
    fmt.Printf("%d bytes @ %d: %s\n", n3, o3, string(b3))
    //io 包提供了一些可以帮助我们进行文件读取的函数。例如,上面的读取可以使用 ReadAtLeast 得到一个更健壮的实现。

    _, err = f.Seek(0, 0)
    check(err)
    //没有内置的回转支持,但是使用 Seek(0, 0) 实现。

    r4 := bufio.NewReader(f)
    b4, err := r4.Peek(5)
    check(err)
    fmt.Printf("5 bytes: %s\n", string(b4))
    //bufio 包实现了带缓冲的读取,这不仅对有很多小的读取操作的能提升性能,也提供了很多附加的读取函数。

    f.Close()
    //任务结束后要关闭这个文件(通常这个操作应该在 Open操作后立即使用 defer 来完成)。

}
$ echo "hello" > /tmp/dat
$ echo "go" >>   /tmp/dat
$ go run reading-files.go 
hello
go
5 bytes: hello
2 bytes @ 6: go
2 bytes @ 6: go
5 bytes: hello

下面我们将看一下写入文件。

写文件

Go 写文件和我们前面看过的读操作有着相似的方式。

package main

import (
    "bufio"
    "fmt"
    "io/ioutil"
    "os"
)

func check(e error) {
    if e != nil {
        panic(e)
    }
}

func main() {

    d1 := []byte("hello\ngo\n")
    err := ioutil.WriteFile("/tmp/dat1", d1, 0644)
    check(err)
    //开始,这里是展示如写入一个字符串(或者只是一些字节)到一个文件。

    f, err := os.Create("/tmp/dat2")
    check(err)
    //对于更细粒度的写入,先打开一个文件。

    defer f.Close()
    //打开文件后,习惯立即使用 defer 调用文件的 Close操作。

    d2 := []byte{115, 111, 109, 101, 10}
    n2, err := f.Write(d2)
    check(err)
    fmt.Printf("wrote %d bytes\n", n2)
    //你可以写入你想写入的字节切片

    n3, err := f.WriteString("writes\n")
    fmt.Printf("wrote %d bytes\n", n3)
    //WriteString 也是可用的。

    f.Sync()
    //调用 Sync 来将缓冲区的信息写入磁盘。

    w := bufio.NewWriter(f)
    n4, err := w.WriteString("buffered\n")
    fmt.Printf("wrote %d bytes\n", n4)
    //bufio 提供了和我们前面看到的带缓冲的读取器一样的带缓冲的写入器。

    w.Flush()
    //使用 Flush 来确保所有缓存的操作已写入底层写入器。
}

运行这端文件写入代码。

$ go run writing-files.go 
wrote 5 bytes
wrote 7 bytes
wrote 9 bytes

$ cat /tmp/dat1
hello
go
$ cat /tmp/dat2
some
writes
buffered

下面我们将看一些文件 I/O 的想法,就像我们已经看过的stdin 和 stdout 流。

行过滤器

一个行过滤器 在读取标准输入流的输入,处理该输入,然后将得到一些的结果输出到标准输出的程序中是常见的一个功能。grep 和 sed 是常见的行过滤器。

这里是一个使用 Go 编写的行过滤器示例,它将所有的输入文字转化为大写的版本。你可以使用这个模式来写一个你自己的 Go行过滤器。

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {

    scanner := bufio.NewScanner(os.Stdin)
    //对 os.Stdin 使用一个带缓冲的 scanner,让我们可以直接使用方便的 Scan 方法来直接读取一行,每次调用该方法可以让 scanner 读取下一行。

    for scanner.Scan() {

        ucl := strings.ToUpper(scanner.Text())
        //Text 返回当前的 token,现在是输入的下一行。

        fmt.Println(ucl)
        //写出大写的行。
    }

    if err := scanner.Err(); err != nil {
        fmt.Fprintln(os.Stderr, "error:", err)
        os.Exit(1)
    }
    //检查 Scan 的错误。文件结束符是可以接受的,并且不会被 Scan 当作一个错误。
}

试一下我们的行过滤器,首先创建多一个有小写行的文件。

$ echo 'hello'   > /tmp/lines
$ echo 'filter' >> /tmp/lines

$ cat /tmp/lines | go run line-filters.go
HELLO
FILTER

然后使用行过滤器来得到大些的行。

命令行参数

命令行参数是指定程序运行参数的一个常见方式。例如,go run hello.go,程序 go 使用了 run 和 hello.go两个参数。

package main

import "os"
import "fmt"

func main() {

    argsWithProg := os.Args
    argsWithoutProg := os.Args[1:]
    //os.Args 提供原始命令行参数访问功能。注意,切片中的第一个参数是该程序的路径,并且 os.Args[1:]保存所有程序的的参数。

    arg := os.Args[3]
    //你可以使用标准的索引位置方式取得单个参数的值。

    fmt.Println(argsWithProg)
    fmt.Println(argsWithoutProg)
    fmt.Println(arg)
}

要实验命令行参数,最好先使用 go build 编译一个可执行二进制文件。

$ go build command-line-arguments.go
$ ./command-line-arguments a b c d
[./command-line-arguments a b c d]
[a b c d]
c

下面我们要看看更高级的使用标记的命令行处理方法。

命令行标志

命令行标志是命令行程序指定选项的常用方式。例如,在wc -l 中,这个 -l 就是一个命令行标志。

package main

import "flag"
import "fmt"
//Go 提供了一个 flag 包,支持基本的命令行标志解析。我们将用这个包来实现我们的命令行程序示例。

func main() {

    wordPtr := flag.String("word", "foo", "a string")
    //基本的标记声明仅支持字符串、整数和布尔值选项。这里我们声明一个默认值为 "foo" 的字符串标志 word并带有一个简短的描述。这里的 flag.String 函数返回一个字符串指针(不是一个字符串值),在下面我们会看到是如何使用这个指针的。

    numbPtr := flag.Int("numb", 42, "an int")
    boolPtr := flag.Bool("fork", false, "a bool")
    //使用和声明 word 标志相同的方法来声明 numb 和 fork 标志。

    var svar string
    flag.StringVar(&svar, "svar", "bar", "a string var")
    //用程序中已有的参数来声明一个标志也是可以的。注意在标志声明函数中需要使用该参数的指针。

    flag.Parse()
    //所有标志都声明完成以后,调用 flag.Parse() 来执行命令行解析。

    fmt.Println("word:", *wordPtr)
    fmt.Println("numb:", *numbPtr)
    fmt.Println("fork:", *boolPtr)
    fmt.Println("svar:", svar)
    fmt.Println("tail:", flag.Args())
    //这里我们将仅输出解析的选项以及后面的位置参数。注意,我们需要使用类似 *wordPtr 这样的语法来对指针解引用,从而得到选项的实际值。
}

测试这个程序前,最好将这个程序编译成二进制文件,然后再运行这个程序。

$ go build command-line-flags.go

word: opt
numb: 7
fork: true
svar: flag
tail: []

注意到,如果你省略一个标志,那么这个标志的值自动的设定为他的默认值。

$ ./command-line-flags -word=opt
word: opt
numb: 42
fork: false
svar: bar
tail: []

位置参数可以出现在任何标志后面。

$ ./command-line-flags -word=opt a1 a2 a3
word: opt
...
tail: [a1 a2 a3]

注意,flag 包需要所有的标志出现位置参数之前(否则,这个标志将会被解析为位置参数)。

$ ./command-line-flags -word=opt a1 a2 a3 -numb=7
word: opt
numb: 42
fork: false
svar: bar
trailing: [a1 a2 a3 -numb=7]

使用 -h 或者 --help 标志来得到自动生成的这个命令行程序的帮助文本。

$ ./command-line-flags -h
Usage of ./command-line-flags:
  -fork=false: a bool
  -numb=42: an int
  -svar="bar": a string var
  -word="foo": a string

如果你提供一个没有使用 flag 包指定的标志,程序会输出一个错误信息,并再次显示帮助文本。

$ ./command-line-flags -wat
flag provided but not defined: -wat
Usage of ./command-line-flags:
...

后面,我们将会看一下环境变量,另一个用于参数化程序的基本方式。

环境变量

环境变量是一个在为 Unix 程序传递配置信息的普遍方式。让我们来看看如何设置,获取并列举环境变量。

package main

import "os"
import "strings"
import "fmt"

func main() {

    os.Setenv("FOO", "1")
    fmt.Println("FOO:", os.Getenv("FOO"))
    fmt.Println("BAR:", os.Getenv("BAR"))
    //使用 os.Setenv 来设置一个键值对。使用 os.Getenv获取一个键对应的值。如果键不存在,将会返回一个空字符串。

    fmt.Println()
    for _, e := range os.Environ() {
        pair := strings.Split(e, "=")
        fmt.Println(pair[0])
    }
    //使用 os.Environ 来列出所有环境变量键值对。这个函数会返回一个 KEY=value 形式的字符串切片。你可以使用strings.Split 来得到键和值。这里我们打印所有的键。
}

运行这个程序,显示我们在程序中设置的 FOO 的值,然而没有设置的 BAR 是空的。键的列表是由你的电脑情况而定的。

$ go run environment-variables.go
FOO: 1
BAR: 

TERM_PROGRAM
PATH
SHELL
...

如果我们在运行前设置了 BAR 的值,那么运行程序将会获取到这个值。

$ BAR=2 go run environment-variables.go
FOO: 1
BAR: 2
...

生成进程

有时,我们的 Go 程序需要生成其他的,非 Go 进程。例如,这个网站的语法高亮是通过在 Go 程序中生成一个pygmentize实现的。让我们看一些关于 Go 生成进程的例子。

package main

import "fmt"
import "io/ioutil"
import "os/exec"

func main() {

    dateCmd := exec.Command("date")
    //我们将从一个简单的命令开始,没有参数或者输入,仅打印一些信息到标准输出流。exec.Command 函数帮助我们创建一个表示这个外部进程的对象。

    dateOut, err := dateCmd.Output()
    if err != nil {
        panic(err)
    }
    fmt.Println("> date")
    fmt.Println(string(dateOut))
    //.Output 是另一个帮助我们处理运行一个命令的常见情况的函数,它等待命令运行完成,并收集命令的输出。如果没有出错,dateOut 将获取到日期信息的字节。

    grepCmd := exec.Command("grep", "hello")
    //下面我们将看看一个稍复杂的例子,我们将从外部进程的stdin 输入数据并从 stdout 收集结果。

    grepIn, _ := grepCmd.StdinPipe()
    grepOut, _ := grepCmd.StdoutPipe()
    grepCmd.Start()
    grepIn.Write([]byte("hello grep\ngoodbye grep"))
    grepIn.Close()
    grepBytes, _ := ioutil.ReadAll(grepOut)
    grepCmd.Wait()
    //这里我们明确的获取输入/输出管道,运行这个进程,写入一些输入信息,读取输出的结果,最后等待程序运行结束。

    fmt.Println("> grep hello")
    fmt.Println(string(grepBytes))
    //上面的例子中,我们忽略了错误检测,但是你可以使用if err != nil 的方式来进行错误检查,我们也只收集StdoutPipe 的结果,但是你可以使用相同的方法收集StderrPipe 的结果。

    lsCmd := exec.Command("bash", "-c", "ls -a -l -h")
    lsOut, err := lsCmd.Output()
    if err != nil {
        panic(err)
    }
    fmt.Println("> ls -a -l -h")
    fmt.Println(string(lsOut))
    //注意,当我们需要提供一个明确的命令和参数数组来生成命令,和能够只需要提供一行命令行字符串相比,你想使用通过一个字符串生成一个完整的命令,那么你可以使用 bash命令的 -c 选项:
}

生成的程序返回和我们直接通过命令行运行这些程序的输出是相同的。

$ go run spawning-processes.go 
> date
Wed Oct 10 09:53:11 PDT 2012

> grep hello
hello grep

> ls -a -l -h
drwxr-xr-x  4 mark 136B Oct 3 16:29 .
drwxr-xr-x 91 mark 3.0K Oct 3 12:50 ..
-rw-r--r--  1 mark 1.3K Oct 3 16:28 spawning-processes.go
 

执行进程

在前面的例子中,我们了解了生成外部进程的知识,当我们需要访问外部进程时时需要这样做,但是有时候,我们只想用其他的(也许是非 Go 程序)来完全替代当前的 Go 进程。这时候,我们可以使用经典的 exec方法的 Go 实现。

package main

import "syscall"
import "os"
import "os/exec"

func main() {

    binary, lookErr := exec.LookPath("ls")
    if lookErr != nil {
        panic(lookErr)
    }
    //在我们的例子中,我们将执行 ls 命令。Go 需要提供我们需要执行的可执行文件的绝对路径,所以我们将使用exec.LookPath 来得到它(大概是 /bin/ls)。

    args := []string{"ls", "-a", "-l", "-h"}
    //Exec 需要的参数是切片的形式的(不是放在一起的一个大字符串)。我们给 ls 一些基本的参数。注意,第一个参数需要是程序名。

    env := os.Environ()
    //Exec 同样需要使用环境变量。这里我们仅提供当前的环境变量。

    execErr := syscall.Exec(binary, args, env)
    if execErr != nil {
        panic(execErr)
    }
    //这里是 os.Exec 调用。如果这个调用成功,那么我们的进程将在这里被替换成 /bin/ls -a -l -h 进程。如果存在错误,那么我们将会得到一个返回值。
}

当我们运行程序时,它会替换为 ls

$ go run execing-processes.go
total 16
drwxr-xr-x  4 mark 136B Oct 3 16:29 .
drwxr-xr-x 91 mark 3.0K Oct 3 12:50 ..
-rw-r--r--  1 mark 1.3K Oct 3 16:28 execing-processes.go

注意 Go 并不提供一个经典的 Unix fork 函数。通常这不是个问题,因为运行 Go 协程,生成进程和执行进程覆盖了fork 的大多数使用用场景。

信号

有时候,我们希望 Go 能智能的处理 Unix 信号。例如,我们希望当服务器接收到一个 SIGTERM 信号时能够自动关机,或者一个命令行工具在接收到一个 SIGINT 信号时停止处理输入信息。这里讲的就就是在 Go 中如何通过通道来处理信号。

package main

import "fmt"
import "os"
import "os/signal"
import "syscall"

func main() {

    sigs := make(chan os.Signal, 1)
    done := make(chan bool, 1)
    //Go 通过向一个通道发送 os.Signal 值来进行信号通知。我们将创建一个通道来接收这些通知(同时还创建一个用于在程序可以结束时进行通知的通道)。

    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
    //signal.Notify 注册这个给定的通道用于接收特定信号。

    go func() {
        sig := <-sigs
        fmt.Println()
        fmt.Println(sig)
        done <- true
    }()
    //这个 Go 协程执行一个阻塞的信号接收操作。当它得到一个值时,它将打印这个值,然后通知程序可以退出。

    fmt.Println("awaiting signal")
    <-done
    fmt.Println("exiting")
    //程序将在这里进行等待,直到它得到了期望的信号(也就是上面的 Go 协程发送的 done 值)然后退出。
}

当我们运行这个程序时,它将一直等待一个信号。使用ctrl-C(终端显示为 ^C),我们可以发送一个 SIGINT 信号,这会使程序打印 interrupt 然后退出。

$ go run signals.go
awaiting signal
^C
interrupt
exiting

退出

使用 os.Exit 来立即进行带给定状态的退出。

package main

import "fmt"
import "os"

func main() {

    defer fmt.Println("!")
    //当使用 os.Exit 时 defer 将不会 执行,所以这里的 fmt.Println将永远不会被调用。

    os.Exit(3)
    //退出并且退出状态为 3。
}

注意,不像例如 C 语言,Go 不使用在 main 中返回一个整数来指明退出状态。如果你想以非零状态退出,那么你就要使用 os.Exit

如果你使用 go run 来运行 exit.go,那么退出状态将会被go捕获并打印。

$ go run exit.go
exit status 3

使用编译并执行一个二进制文件的方式,你可以在终端中查看退出状态。

$ go build exit.go
$ ./exit
$ echo $?
3

注意我们程序中的 ! 永远不会被打印出来。

加入1KE学习俱乐部

1KE学习俱乐部是只针对1KE学员开放的私人俱乐部
标签:
Go
课程交流
charlidsun
2015-09-12

代码注释很详细.赞.