Golang学习之并发编程

一、并发编程概述

1.1 并行和并发

1.1.1 并行 parallel

在同一时刻,有多条指令在多个处理器上同时执行。

1.1.1.1 并行的两大特性

1.多进程、多核心。
2.物理上同时发生的过程。

1.1.1.2 并行的简要示例图

并行简要示例图
并行需要硬件的支持,有块 128 核的 CPU 就能同时处理 128 个任务。

1.1.2 并发 concurrency

并发是时间片轮转。在同一时刻只能有一条指令执行,但多个进程指令被快速地轮换执行,使得在宏观上具有多个进程同时执行的效果。但在微观上并不是同时执行,只是把时间分成若干段,使多个任务快速交替地执行。

1.1.2.1 并发的两大特性

1.单进程。
2.逻辑上同时发生的过程。

1.1.2.2 时间片轮转

举例解释:一个 CPU 给一个任务分配 1 秒的时间处理任务,1 秒后不再处理此任务而去处理另一个任务,由此轮转着进行。也就是,一个 CPU 处理任务的时间,根据这个时间,间隔交替执行多个任务。
CPU 处理任务的效率极高,通常是几毫秒内(甚至只是几纳秒)就能处理完一个任务,几毫秒的时间切换,人的肉眼是看不出交替的过程,也就是看起来感觉像是并行的。
并发是技术层面的工作:在有限的资源内,高效地利用 CPU 资源。

1.1.2.3 并发的简要示例图

并发简要示例图

1.2 使用咖啡机理解并行与并发

拿现实生活中咖啡机的例子,初步理解并行与并发。

1.2.1 并行演示

有两台咖啡机,两个队列同时使用两台咖啡机。
并行咖啡机示例图

1.2.2 并发演示

两个队列交替使用同一台咖啡机。
只有一台咖啡机,一个队列的一个人拿完咖啡,另一个队列的另一个人拿咖啡。交替着进行
并发咖啡机示例图

1.3 Go 语言的并发机制

所谓并发,就是交替着执行多个几个任务。
Golang 为并发编程而内置的上层 API 基于 CSP (Communicating Sequential Processes, 顺序通信进程)模型,显示锁是可以避免的。
Golang 使用了 goroutine (微线程,也可以称呼为:Go协程)来实现并发机制。
Golang 通过安全的通道发送和接收数据以实现同步。

二、goroutine

和传统基于 OS 进程和线程不同,Go 语言的并发是基于用户态的并发,使用 goroutine 协程模型。
goroutine 其实是 协程 —— Go 协程,Go 语言的运行时(也就是 runtime)会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用 CPU 的性能。
可以把一个 goroutine 当做一个任务,一个 goroutine 就是一个新的任务。
协程比线程更小,十几个 goroutine 可能体现在底层就是 5、6 个线程,执行 goroutine 只需极少的栈内存(4 ~ 5 KB),并根据实际数据进行伸缩。

2.1 创建goroutine

只需要在函数调用语句前添加 go 关键字,就可以创建并发执行单元,调度器会自动将其安排到合适的系统线程上执行。
通过 go 关键字创建的 goroutine,可以称为子协程、工作协程。
可以把 func main() {} 理解成主协程,main() 主函数是一个 main goroutine

2.1.1 最基本的示例

新建一个 go 协程(新建一个新任务)。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import (
"fmt"
"time"
)

func newTask() {
for {
fmt.Println("this is newTask")
time.Sleep(time.Second)
}
}

func main() {
// 不能放到 for 语句块的下面,不然程序就一直在执行 for 死循环,永远不会去开启 go 协程。
// 新建一个 go 协程,新建一个任务。这个是新建的任务,原来的任务就是这个主函数:main 主协程
// 任务创建完毕后,程序继续往下走。
go newTask()

for {
fmt.Println("this is main")
time.Sleep(time.Second)
}
}

/*
运行结果:
this is main
this is newTask
this is newTask
this is main
this is main
this is newTask
this is newTask
this is main
...
*/

打印顺序不可控是因为任务调度的原因造成:
遇到 go 关键字,系统就会新建一个 go 协程。子协程是一个新任务,主函数本身就是一个任务。两个任务交替着执行,就看谁先被调度器调度。两个任务轮转着执行。

2.2 主协程退出,子协程也一起退出

主协程退出,子协程也一起退出。也可以称:主协程结束,子协程跟着一起结束。
main() 函数是主协程;其他由 go 关键字创建的协程叫子协程。

2.2.1 示例1:主协程结束,子协程跟着一起结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import (
"fmt"
"time"
)

func main() {
// 新建一个新任务
go func() {
i := 0
for {
i++
fmt.Println("sub-goroutine, i=", i)
time.Sleep(time.Second)
}
}() // 匿名函数别忘了加圆括号()调用

// 主任务
i := 0
for {
i++
fmt.Println("this is main, i=", i)
time.Sleep(time.Second)

// 2 秒后退出这个 for 循环
if i == 2 {
break
}
}
}

/*
运行结果:
this is main, i= 1
sub-goroutine, i= 1
this is main, i= 2
sub-goroutine, i= 2
sub-goroutine, i= 3
*/

2 秒后,主协程内的 for 循环退出,接下来也没有其他代码,主协程(main() 函数)就结束了。此时,虽然子协程是一个死循环,但主协程已经结束了,那么子协程也跟着结束。

2.2.2 示例2:主协程先退出,导致子协程没来得及调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import (
"fmt"
"time"
)

func main() {
// 新建一个新任务
go func() {
i := 0
for {
i++
fmt.Println("sub-goroutine, i=", i)
time.Sleep(time.Second)
}
}()
}

运行结果什么都没有的原因:
程序从 main() 函数入口处进入,main() 函数就是一个主协程。看到 go 关键字,就会去新建一个新的子协程。新建完后程序就会往下走,发现没有代码了,走到了主协程(main() 函数)的结束处,那么主协程(main() 函数)就结束了。main() 结束,整个程序结束,子协程只是被创建了但还没来得及运行,整个程序就已经结束了。整个程序结束了,子协程也就不复存在了!就相当于:想玩电脑游戏,刚启动游戏,游戏画面都还没有出来,跳闸断电关机了,游戏还能继续在电脑上运行吗?

2.3 runtime 包

2.3.1 runtime.Gosched

runtime.Gosched(),让出时间片。用于让出 CPU 时间片(让出当前 goroutine 的执行权限),调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
简而言之:遇到 runtime.Gosched() 这行代码,让其他协程先执行完,然后再回到原来的调用者协程中,继续往下执行。
示例1:先看一个反面例子,程序执行完就结束,子协程没来得及执行就已经退出了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
go func() {
for i := 0; i < 5; i++ {
fmt.Println("go, i=", i)
}
}()

for i := 0; i < 2; i++ {
fmt.Println("main, i=", i)
}

}

/*
运行结果:
main, i= 0
main, i= 1
*/

注意:因为调度的原因,有时候是可以打印出子协程的内容,甚至偶尔能让子协程运行完成。这是因为底层调度器的关系,先调度了子协程那么就先运行子协程的代码,先调度主协程那么就运行主协程。主协程运行结束,系统资源要回收前那一瞬间,调度器又调度了子协程,所以能运行子协程,但是当调度器再次回到主协程时,主协程因为之前已执行完毕,所以退出了。主协程退出,整个程序结束,子协程跟着结束。
接下来使用 runtime.Gosched(),让其他的协程先运行完:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import (
"fmt"
"runtime"
)

func main() {
go func() {
for i := 0; i < 5; i++ {
fmt.Println("go, i=", i)
}
}()

for i := 0; i < 2; i++ {
runtime.Gosched() // 让出时间片,让其他协程先执行完,然后再回来执行此协程

fmt.Println("main, i=", i)
}

}

/*
运行结果:
go, i= 0
go, i= 1
go, i= 2
go, i= 3
go, i= 4
main, i= 0
main, i= 1
*/

此时,就不会发生示例 1 中的情况。这里,遇到 runtime.Gosched() 就先不执行主协程,让子协程先执行,子协程执行完了,回来后,再继续执行主协程。

2.3.2 runtime.Goexit

runtime.Goexit(),立即终止当前 goroutine 执行。调度器确保所有已注册 defer 延迟调用被执行。
示例1:普通写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func test() {
defer fmt.Println("test cccccccc")

fmt.Println("dddddd")
}

func main() {
go func() {
fmt.Println("aaaaa")

test()

fmt.Println("bbbbb")
}()

//死循环的目的是为了不让主协程结束
for {
}
}

/*
运行结果:
aaaaa
dddddd
test cccccccc
bbbbb

*/

程序正常执行,开启一个子协程,第一步打印出”a…”,调用函数test,第二步打印”d…”,test函数结束前执行第三步打印”test c…”,最后打印”b…”
示例2:return 终止函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func test() {
defer fmt.Println("test cccccccc")

return //终止函数

fmt.Println("dddddd")
}

func main() {
go func() {
fmt.Println("aaaaa")

test()

fmt.Println("bbbbb")
}()

//死循环的目的是为了不让主协程结束
for {
}
}

/*
运行结果:
aaaaa
test cccccccc
bbbbb

*/

test() 函数中,return 会终止这个test函数,所以”d…”不会打印出来。
示例3:runtime.Goexit 退出当前整个协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import (
"fmt"
"runtime"
)

func test() {
defer fmt.Println("test cccccccc")

runtime.Goexit() //终止当前整个协程

fmt.Println("dddddd")
}

func main() {
go func() {
fmt.Println("aaaaa")

test()

fmt.Println("bbbbb") //因为被终止,所以不执行
}()

// 死循环的目的是为了不让主协程结束
for {
}
}

/*
运行结果:
aaaaa
test cccccccc

*/

returnruntime.Goexit() 的区别:return 只是终止当前这个函数;runtime.Goexit() 终止的是当前整个协程。
示例4:runtime.Goexit() 终止当前整个协程,它不会追溯到其他的协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import (
"fmt"
"runtime"
)

//由gor1()协程中的代码调用
func test() {
defer fmt.Println("test ccc")

runtime.Goexit() //终止gor1()整个协程

fmt.Println("test ddd")
}

func gor1() {
i := 0
for {
i++
fmt.Println("gor1..., i=", i)

if i == 2 {
test()
}
}
fmt.Println("gor1..., finished.")
}

func main() {

//新建一个子协程
go func() {
fmt.Println("aaa")

//又新建一个子协程
go gor1() //新建后代码往下走,并不会立即调度执行这个协程里的代码

fmt.Println("bbb")
}()

//死循环的目的是为了不让主协程结束
for {
}
}

/*
运行结果:
aaa
bbb
gor1..., i= 1
gor1..., i= 2
test ccc

*/

新建的第一个协程是匿名函数的协程,当新建第二个协程的时候,它们就是两个独立的协程了。runtime.Goexit() 终止的是当前整个协程,而不会追溯到其他的协程。看起来,gor1() 这个协程是在匿名函数协程中的,被匿名函数包裹起来了,感觉上是 gor1() 终止了,匿名函数也会被终止。但是,它们是两个独立的协程。所以当gor1()这个协程结束的时候,只会结束当前gor1() 这个协程,不会追溯到匿名函数这个协程。

2.3.3 runtime.GOMAXPROCS

runtime.GOMAXPROCS(int) 设置可以并行计算的 CPU 核数的最大值,并返回之前有几个 CPU 参与运算。
就是:使用多少核来运行程序。设置多个 CPU,让时间片轮转速度更快一些,执行效率更高。
示例1:以单核 CPU 运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import (
"fmt"
"runtime"
)

func main() {
cores := runtime.GOMAXPROCS(1) // 指定以单核 CPU 运算,返回之前有几个 CPU 参与运算
fmt.Println("how many cores:", cores)

for {
go fmt.Print(1)

fmt.Print(0)
}
}

只有单核运算的情况下,时间片轮转是:这个任务运行一段时间,另一个任务运行一段时间,交替着运行任务。所以,能看到下图中,同一时间只会输出一个值,因为在那个时间中,只处理那一个任务:
单核 CPU 运算
示例2:多核 CPU 运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import (
"fmt"
"runtime"
)

func main() {
cores := runtime.GOMAXPROCS(2) // 指定 2 个 CPU 运算,返回之前有几个 CPU 在参与运算
fmt.Println("how many cores:", cores)

for {
go fmt.Print(1)

fmt.Print(0)
}
}

不会出现示例 1 中,一大坨 0、1 的情况。因为是两个 CPU 在同时处理任务。
多核 CPU 运算

三、多任务资源竞争的问题

场景模拟:两个人在用同一台打印机,打印内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import (
"fmt"
"time"
)

// 模拟一个打印机,按字符串的顺序打印每个字符
// 这个打印机属于公共资源
func Printer(str string) {
for _, c := range str {
fmt.Printf("%c", c) //Print(c)打印的是每个字符的ASCII码值
time.Sleep(time.Second)
}
fmt.Println()
}

func person1() {
Printer("hello")
}

func person2() {
Printer("world")
}

func main() {
// 新建 2 个子协程,模拟 2 个人同时使用打印机
go person1()
go person2()

// 为了不让主协程结束,特意放了一个死循环
for {
}
}

/*
运行结果:
hweolrllod
*/

多任务时经常遇到的问题:资源竞争,抢着用同一个资源。
如下图所示,两个子协程同时在使用一个资源,person1() 打印了 “h”,person2() 打印了 “w”,person1() 又来打印了 “e”,person2() 又来打印了 “o” …… 两个协程抢着用同一个资源,所以最终结果就乱了。
资源竞争
解决方案:同步。一个执行完了再执行另外一个,互斥。互斥需要用到 channel

四、channel

channel 主要解决资源竞争的问题,以及两个协程之间的数据交互。实现:同步和数据交互。
goroutine 运行在相同的地址空间,访问共享内存必须做好同步。goroutine 之间的数据交互需要用到 channel
goroutine 奉行:通过通信来共享内存,而不是共享内存来通信
channelCSP 模型的具体实现,用于多个 goroutine 之间的通讯,其内部实现了同步,确保并发的安全。
channel 是引用类型,函数参数传递时,是引用传递(传址)。

4.1 channel 类型概述

channel 是一个对应 make() 创建的底层数据结构的引用。
channel 属于引用传递(传址),调用者和被调用者引用的是同一个 channel
定义一个 channel 时,必须指定发送到 channel 的值的类型。

4.2 channel 创建

4.2.1 基本语法

1.make(chan Type)
2.make(chan Type, capacity)
capacity=0 的时候,channel 是无缓冲、阻塞读写的。
capacity>0 的时候,channel 有缓冲、非阻塞的。

4.2.2 channel 的操作符:<-

channel 通过操作符 <- 来接收和发送数据。

1
2
3
4
channel <- value // 发送 value 到 channel
<-channel // 接收数据并将其丢弃
x := <-channel // 从 channel 中接收数据,并赋值给变量x
x, ok := <-channel // 接收数据赋值给x,同时检查通道是否已关闭或是否为空

capacity=0 或不指定的时候,channel 接收和发送数据都是阻塞的(没数据之前,它就等着写入数据才往下走),除非另一端已经准备好。这样使得 goroutine 同步变得更加的简单,不需要显示的 ok

4.3 使用 channel 实现同步

同步:有先后顺序地执行,一个执行完再执行另外一个。通道没有数据就会阻塞!
以解决第 3 部分打印机的问题为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import (
"fmt"
"time"
)

// 管道全局变量,方便调用
var ch = make(chan int) // 管道传递的数据类型根据实际需要来指明

// 模拟一个打印机,按字符串的顺序打印每个字符
// 这个打印机属于公共资源
func Printer(str string) {
for _, c := range str {
fmt.Printf("%c", c) // Print(c) 打印的是每个字符的 ASCII 码值
time.Sleep(time.Second / 2)
}
fmt.Println()
}

func person1() {
Printer("hello")
ch <- 1 // 给管道写数据,发送
}

func person2() {
<-ch // 从管道取数据,接收。如果管道没有数据就会阻塞
Printer("world")
}

func main() {
// 新建2个子协程,模拟2个人同时使用打印机
go person1()
go person2()

// 为了不让主协程结束,特意放了一个死循环
for {
}
}

/*
运行结果:
hello
world

Process finished with exit code 2
*/

4.4 通过 channel 实现同步和数据交互

4.4.1 死锁错误演示1

程序走完了,但依然没有给管道写数据,管道中没有数据能够接收到,那么程序永远阻塞在那里不会往下走,引发了死锁错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func main() {
defer fmt.Println("main-goroutine finished.")

// 创建一个channel
ch := make(chan string)

// 新建一个子协程,懒得写函数调用可以使用匿名函数
go func() {
for i := 0; i < 2; i++ {
fmt.Println("sub-goroutine, i=", i)
}
fmt.Println("sub-goroutine finished.")
}()

str := <-ch // 管道中取不到数据,就会永远卡在这里,造成死锁
fmt.Println("main-goroutine, str=", str)
}

/*
运行结果:
sub-goroutine, i= 0
sub-goroutine, i= 1
sub-goroutine finished.
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
F:/goProject/src/myTest/main.go:19 +0x112

Process finished with exit code 2
*/

4.4.2 死锁错误演示2

不论是有缓冲通道还是无缓冲通道,通道中任意一方永久性阻塞,就会引发死锁错误。
死锁错误演示
死锁错误演示

4.4.3 解决死锁错误的方案:给管道写入数据

往管道里写入数据,Golang 内部立马能够检测到。管道接收到了数据就不会阻塞了,就能往下走了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func main() {
defer fmt.Println("main-goroutine finished.")

// 创建一个channel
ch := make(chan string)

// 新建一个子协程,懒得写函数调用可以使用匿名函数
go func() {
defer fmt.Println("sub-goroutine closed.")

for i := 0; i < 2; i++ {
fmt.Println("sub-goroutine, i=", i)
}

ch <- "sub-goroutine has finished work." // 给管道写入数据
}()

str := <-ch // 能从管道中取到数据了
fmt.Println("main-goroutine, str=", str)
}

/*
运行结果:
sub-goroutine, i= 0
sub-goroutine, i= 1
sub-goroutine closed.
main-goroutine, str= sub-goroutine has finished work.
main-goroutine finished.
*/

4.5 无缓冲 channel

无缓冲的通道 unbuffered channel 是指在接收前没有能力保存任何值的通道。
无缓冲的通道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。如果两个 goroutine 没有同时准备好,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。这种交互行为就是同步的,任意一个操作都无法离开另一个操作,不能单独存在。
特点:

  • 两边都阻塞
  • 管道中不能存东西
  • 交互之前阻塞,交互过程中阻塞,交互完成了才往下走
    无缓冲channel

4.5.1 创建无缓冲 channel

语法:make(chan Type)。不要写 capacity,或者 capacity=0

4.5.2 计算channel的函数

len(ch) 缓冲区剩余数据的数量。
cap(ch) 缓冲区大小。

4.5.3 示例

之前的例子都是无缓冲的 channel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import (
"fmt"
"time"
)

func main() {
// 定义一个无缓冲的channel
ch := make(chan int)
fmt.Printf("len(ch)=%d, cap(ch)=%d\n", len(ch), cap(ch))

// 新建一个子协程
go func() {
for i := 0; i < 3; i++ {
fmt.Println("sub-goroutine, i=", i)
ch <- i // 往管道中放入内容
}
}()

time.Sleep(time.Second * 2)

for i := 0; i < 3; i++ {
num := <-ch // 从管道中取内容,没有内容前会阻塞
fmt.Println("main-goroutine, num=", num)
}
}

/*
运行结果:
len(ch)=0, cap(ch)=0
sub-goroutine, i= 0
sub-goroutine, i= 1
main-goroutine, num= 0
main-goroutine, num= 1
sub-goroutine, i= 2
main-goroutine, num= 2
*/

4.5.4 通过 channel 共享 goroutine 的变量

多个 goroutine 之间的变量共享示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import (
"fmt"
"time"
)

// 生产者模型
func Produce(name string, ch chan<- string) {
for i := 0; ; i++ {
ch <- fmt.Sprintf("%s's product code: %d", name, time.Now().Unix())
time.Sleep(1 * time.Second)
}
}

// 消费者模型
func Customer(name string, ch <-chan string) {
for {
fmt.Printf("%s cost %s\n", name, <-ch)
}
}

func main() {
ch := make(chan string)

// 两个子协程用来生产
go Produce("Sally", ch)
go Produce("Rita", ch)

// 启动消费者模型,避免主协程提前退出
Customer("Barry", ch)
}

/*
运行结果:
Barry cost Rita's product code: 1601643297
Barry cost Sally's product code: 1601643297
Barry cost Sally's product code: 1601643298
Barry cost Rita's product code: 1601643298
Barry cost Rita's product code: 1601643299
Barry cost Sally's product code: 1601643299
Barry cost Sally's product code: 1601643300
Barry cost Rita's product code: 1601643300
Barry cost Sally's product code: 1601643301
Barry cost Rita's product code: 1601643301
...
*/

4.6 有缓冲的 channel

有缓冲的通道 buffered channel 是一种在被接收前能存储一个或多个值的通道。
给定了一个缓冲区容量的通道,这个通道就是异步的。只要缓冲区未满还可以用于发送数据,或者接收方还能接收到数据,那么通信就可以无阻塞地进行。
有缓冲的 channel 发生阻塞的两种情况:
1.缓冲区满了,放不下任务数据了,发送方就会阻塞,等待接收方去取出至少一个数据。
2.缓冲区里面还没有任何的数据,接收方没办法接收到数据,接收方阻塞等待发送方把数据放入管道。
有缓冲channel

4.6.1 创建有缓冲的 channel

语法:make(chan Type, capacity),并且 capaciy>0 即可。

4.6.2 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import (
"fmt"
"time"
)

func main() {
// 定义一个有缓冲的 channel,里面能放 3 个东西
ch := make(chan int, 3)
fmt.Printf("len(ch)=%d, cap(ch)=%d\n", len(ch), cap(ch))

// 新建一个子协程
go func() {
for i := 0; i < 3; i++ {
fmt.Printf("sub-goroutine [%d], len(ch)=%d, cap(ch)=%d\n", i, len(ch), cap(ch))
ch <- i // 管道中一次性可以放入3个东西
}
}()

time.Sleep(time.Second * 2)

for i := 0; i < 3; i++ {
num := <-ch // 从管道中也可以一次性取 3 个东西
fmt.Println("main-goroutine, num=", num)
}
}

/*
运行结果:
len(ch)=0, cap(ch)=3
sub-goroutine [0], len(ch)=1, cap(ch)=3
sub-goroutine [1], len(ch)=2, cap(ch)=3
sub-goroutine [2], len(ch)=3, cap(ch)=3
main-goroutine, num= 0
main-goroutine, num= 1
main-goroutine, num= 2
*/

4.7 两个类型 channel 的总结

1.无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交互;有缓冲的通道没有这样的保证。
2.无缓冲通道,发送方会阻塞直到接收方从通道中接收了值。
3.有缓冲的通道允许发送端的数据发送和接收端的数据获取处于异步状态,发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
4.由于缓冲区的大小是有限的,所以还是必须要有接收端来接收数据。否则缓冲区一满,发送端就无法再发送数据了。
5.有缓冲的通道,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。
6.有缓冲的通道,只要通道没满,放几个就可以拿几个。通道满了,就必须取出一个才能继续往里面放数据。

4.8 关闭 channel

内部能够检查到通道是否被关闭了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func main() {
ch := make(chan int)

go func() {
for i := 0; i < 5; i++ {
ch <- i
}

// 没有数据写入通道时,必须关闭通道
// 不然调用者接收不到数据就会引发死锁错误
close(ch)
}()

// 不需要再指定 5 次了,通过判断通道是否被关闭来结束程序
for {
// 如果 ok 为 true,表明管道没有被关闭
if num, ok := <-ch; ok {
fmt.Println("num=", num)
} else {
fmt.Println("channel has been closed.")
break
}
}
}

/*
运行结果:
num= 0
num= 1
num= 2
num= 3
num= 4
channel has been closed.
*/

注意事项:
1.channel 不像文件那样需要经常关闭,只有当确定没有任何发送数据了,或者想显示地结束 range 循环之类的采取关闭 channel
2.关闭 channel 后,无法再向 channel 发送数据,否则引发 panic 错误且导致接收立即返回零值。
3.管道关闭后,可以继续向 channel 接收数据。
4.对于 nilchannel,无论收发都会被永久阻塞,引发死锁错误。

4.9 通过range遍历channel

更加简便,遍历时检测到管道被关闭了,就自动跳出循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func main() {
ch := make(chan int, 1)

go func() {
for i := 0; i < 5; i++ {
ch <- i
}

// 没有数据写入通道时,必须关闭通道
// 不然其他goroutine接收不到数据就会引发死锁错误
close(ch)
}()

// 无需显示判断,遍历channel时会自动检测,如果管道被关闭了,则自动跳出循环
for num := range ch {
fmt.Println("num=", num)
}
}

/*
运行结果:
num= 0
num= 1
num= 2
num= 3
num= 4
*/

4.10 单向channel

管道默认是双向的,可读可写。

4.10.1 单向channel变量的声明

就看 <- 箭头放在 chan 这个关键字的哪一边。
没放就是普通的双向通道,放在左边是只读(<-ch 表示取数据),放在右边是只写(ch<- 表示放入数据)。

1
2
3
var ch1 chan int // 正常的双向通道
var ch2 chan<- float64 // 单向channel,只用于写float64数据
var ch3 <-chan int // 单向channel,只用于读取int数据

4.10.2 单向 channel 的特点

1.可以将普通的双向通道隐式转换为单向通道,不能将单向通道转换为普通的双向通道
2.只写的只做写入数据的事情,只读的只做取数据的事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
// 普通的双向通道
ch1 := make(chan int)

var writeCh chan<- int = ch1
writeCh <- 999
<-writeCh // 报错:invalid operation: <-writeCh (receive from send-only type chan<- int)

var readCh <-chan int = ch1
<-readCh
readCh <- 999 // 报错:invalid operation: readCh <- 999 (send to receive-only type <-chan int)

var ch2 chan int
ch2 = writeCh // 报错:cannot use writeCh (type chan<- int) as type chan int in assignment
}

4.10.3 单向 channel 的应用

生产者&消费者模型:生产一个消费一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func producer(write chan<- int) { // 此通道只写,不能读取
for i := 0; i < 10; i++ {
write <- i * i
}
}

func consumer(read <-chan int) { // 此通道只读,不能写入
for data := range read {
fmt.Println("consumer, data=", data)
}
}

func main() {
ch := make(chan int)

// 生产者,生产数字并放入管道
go producer(ch) //channel是引用传递(传址)

// 消费者,从channel中读取数字并打印消费
go consumer(ch) //channel是引用传递(传址)

// 为了不让主协程退出,特意写了死循环
for {
}
}

/*
运行结果:
consumer, data= 0
consumer, data= 1
consumer, data= 4
consumer, data= 9
consumer, data= 16
consumer, data= 25
consumer, data= 36
consumer, data= 49
consumer, data= 64
consumer, data= 81

Process finished with exit code 2
*/

取数据的时候,没有数据就会在那边阻塞。等0*0放入管道的时候,内部立马检测到了有数据可以去取,才能去取到数据并打印出来。以此类推循环 10 次:放入一个,读取一个。

五、定时器

5.1 Timer

Timer是一个定时器,代表未来的一个单一事件。告诉timer需要等待多长时间,它提供了一个channel,时间到往这个channel写入当前的时间值。

5.1.1 创建一个 Timer

需要用到内建的 time 包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"fmt"
"time"
)

func main() {
fmt.Println("current time:", time.Now())

// 创建一个Timer,设定2秒。2秒后,往timer通道发送当前的时间值(当前时间)
timer := time.NewTimer(2 * time.Second)
// 2 秒后会往timer通道写入数据

// 需要手动地显示地获取数据,不写这行代码就不会产生延时效果
t := <-timer.C // .C 表示读取数据,没有数据写入之前会阻塞在这里

fmt.Println("t=", t)
}

/*
运行结果:
current time: 2020-02-22 10:22:14.3789342 +0800 CST m=+0.001995101
t= 2020-02-22 10:22:16.3890326 +0800 CST m=+2.012093501
*/

NewTimer() 是过一段时间后,往管道中写入数据。如果没有显示地 <- 去取数据,内部检测不到取数据的操作,既然你不想要拿数据,那它也就不会往管道里放数据了。有了取值操作 <-,内部就知道你想要过一段时间后,从管道中取数据,定时器才会生效。

5.1.2 Timer 只会产生一次事件

Timer只会响应一次。时间到了,只会产生一次事件,只会写入一次数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import (
"fmt"
"time"
)

func main() {
timer := time.NewTimer(time.Second)
for i := 0; i < 10; i++ {
<-timer.C // 报错:死锁错误
fmt.Println("时间到")
}
}

/*
运行结果:
时间到
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
F:/goProject/src/myTest/main.go:11 +0x54

Process finished with exit code 2
*/

只写入了一次数据,那也只能拿到一次数据。第二次开始一直阻塞在取数据的地方,程序永远不会往下走,引发死锁错误。

5.1.3 Timer 实现延时功能

有3种方式可以实现延时功能:Sleep(), time.NewTimer(), time.After()
示例1:Sleep(),最简单粗暴的方式

1
2
3
4
5
6
7
8
9
10
11
func main() {
fmt.Println("当前时间:", time.Now())
time.Sleep(2 * time.Second)
fmt.Println("时间到,当前时间:", time.Now())
}

/*
运行结果:
当前时间: 2020-02-22 10:38:33.8267119 +0800 CST m=+0.001994701
时间到,当前时间: 2020-02-22 10:38:35.8363347 +0800 CST m=+2.011617501
*/

示例2:time.NewTimer(),会产生一个值写入管道 C,需要手动从管道 C 中接收时间值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
fmt.Println("当前时间:", time.Now())

timer := time.NewTimer(2 * time.Second)

// 没有这行语句,就不会有延时效果
t := <-timer.C // 内部会把延时后的当前时间写入管道C中,需要手动<-从管道C中读取出来

fmt.Println("时间到,当前时间:", t)
}

/*
运行结果:
当前时间: 2020-02-22 10:42:10.0294307 +0800 CST m=+0.001995601
时间到,当前时间: 2020-02-22 10:42:12.0387286 +0800 CST m=+2.011293501
*/

示例3:time.After(),常用于超时处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
fmt.Println("当前时间:", time.Now())

// 设置 2 秒延时,阻塞 2 秒。2 秒后产生一个事件(就是往管道里写入延时后的当前时间)
// 直接使用<-操作符取值即可
t := <-time.After(2 * time.Second) // 没有 <- 去取值,就不会有延时的效果,不要忘了写上

fmt.Println("时间到,当前时间:", t)
}

/*
运行结果:
当前时间: 2020-02-22 11:02:20.2412903 +0800 CST m=+0.001994901
时间到,当前时间: 2020-02-22 11:02:22.2522149 +0800 CST m=+2.012919501
*/

5.1.4 Timer注意事项

Timer 需要手动地显示地使用操作符 <- 去管道 C 中接收数据,否则就不会有延时的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
fmt.Println("当前时间:", time.Now())

timer := time.NewTimer(2 * time.Second)
//t := <-timer.C //内部会把延时后的当前时间写入管道C中,需要手动从管道C中读取出来
fmt.Println("timer=", timer)

fmt.Println("时间到,当前时间:", time.Now())
}

/*
运行结果:
当前时间: 2020-02-22 10:47:51.8096677 +0800 CST m=+0.001994301
timer= &{0xc0000ae000 {5797568 0 432376605941300 0 0x491d10 0xc0000ae000 0}}
时间到,当前时间: 2020-02-22 10:47:51.8176459 +0800 CST m=+0.009972501
*/

上例中,根本没有预期的 2 秒钟的延时。
个人自己猜测了一下原因:操作符 <- 取值时,没有数据的时候是会阻塞的。计时器是时间到了,就把当前时间将往管道 C 中发送。如果没有操作符 <- 去取值,Golang 内部会认为你不要这个值,既然你不要这个值,那我为什么还要费力等一段时间后再往管道中放入数据?
timer.C 手动地显示地获取一下,就实现效果了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
fmt.Println("当前时间:", time.Now())

timer := time.NewTimer(2 * time.Second)
t := <-timer.C // 内部会把延时后的当前时间写入管道C中,需要手动从管道C中读取出来

fmt.Println("时间到,当前时间:", t)
}

/*
运行结果:
当前时间: 2020-02-22 10:56:08.9670945 +0800 CST m=+0.001995701
时间到,当前时间: 2020-02-22 10:56:10.9765383 +0800 CST m=+2.011439501
*/

5.1.5 Timer停止

timer.Stop() 将定时器停止、作废,使定时器失效。
示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main() {
fmt.Println("current time:", time.Now())
timer := time.NewTimer(2 * time.Second)

go func() {
<-timer.C
fmt.Println("定时器时间到,子协程可以打印了. current time:", time.Now())
}()

timer.Stop() // 停止定时器,定时器无效了

for {
fmt.Println("1")
time.Sleep(2 * time.Second)
}
}

/*
运行结果:
current time: 2020-02-22 15:07:58.7841598 +0800 CST m=+0.001995101
1
1
1
1
1

Process finished with exit code 2
*/

上例中:主协程中让定时器失效,定时器永远不会往子协程中的管道 C 发送数据。主协程和子协程是两个独立的任务:主协程只是个死循环,调度器调度主协程时,每隔2秒无限打印。而调度子协程时,因为没有任何数据可以接收到,所以就一直阻塞在 <-timer.C 那里。个人猜想,因为有两个任务在不停切换,所以它并不会引发死锁错误。
示例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
fmt.Println("current time:", time.Now())
timer := time.NewTimer(2 * time.Second)

timer.Stop() // 停止定时器,定时器无效了
t := <-timer.C // 定时器在这之前就已失效,就接收不到任何值了,永远阻塞在此,引发死锁错误
fmt.Println("定时器时间到. t=", t)
}

/*
运行结果:
current time: 2020-02-22 15:00:37.0752347 +0800 CST m=+0.001987401
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
F:/goProject/src/myTest/main.go:13 +0x154

Process finished with exit code 2
*/

上例中,可能因为只有一条主协程的原因,而引发了死锁错误。

5.1.6 Timer重置

timer.Reset() 重新设定一个 Timer 的过期时间,会把之前设定的时间给覆盖掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
fmt.Println("current time:", time.Now())

timer := time.NewTimer(5 * time.Second)
timer.Reset(time.Second) // 重新设定为1秒钟过期,会覆盖原有的时间限定

<-timer.C // 取值,为了让它阻塞在此

fmt.Println("时间到,current time:", time.Now())
}

/*
运行结果:
current time: 2020-02-22 14:54:52.7534029 +0800 CST m=+0.001994501
时间到,current time: 2020-02-22 14:54:53.7637002 +0800 CST m=+1.012291801
*/

5.2 Ticker

Ticker 是一个定时触发的计时器,它会以一个间隔 intervalchannel 发送一个事件(当前时间),channel 的接收者可以以固定的时间间隔从 channel 中读取事件(当前时间)。
Ticker 是时间到就产生一个事件,一直如此循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func main() {

// 定义一个Ticker,每隔2秒循环一个事件(往管道C中写入当前时间值)
ticker := time.NewTicker(2 * time.Second)

i := 0
for {
i++

fmt.Println("current time:", <-ticker.C) // 从管道C中读取事件(当前时间),不然计时器无法生效

// i=5 的时候,计时器停止,并退出循环。退出循环后,没有接下来的代码了,就是结束程序了
if i == 5 {
ticker.Stop()
break
}
}
}

/*
运行结果:
current time: 2020-02-22 15:27:03.2978298 +0800 CST m=+2.002727901
current time: 2020-02-22 15:27:05.2976192 +0800 CST m=+4.002517301
current time: 2020-02-22 15:27:07.2972701 +0800 CST m=+6.002168201
current time: 2020-02-22 15:27:09.2989453 +0800 CST m=+8.003843401
current time: 2020-02-22 15:27:11.2977031 +0800 CST m=+10.002601201
*/

5.3 Timer和Ticker区别

Timer 时间到了,只会产生一次事件。
Ticker 会循环产生事件。

六、select

通过 select 可以监听 channel 上的数据流动(数据流动方向),用来处理异步 IO。特别注意:select 中,每个 case 语句都必须是一个 IO 操作。
在一个 select 语句块中,Golang 会按顺序从头到尾评估每一个发送和接收的语句。如果其中的任意一条语句可以继续执行(即没有阻塞),那么就从那些可以执行的语句中任意选择一条来使用。
如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种情况:
1.如果给出了 default 语句,那么就会执行 default 语句,同时程序的执行会从 select 语句后的语句中恢复。
2.如果没有 default 语句,那么 select 语句将被阻塞,直到至少一个通信到达后,才会执行下去。

6.1 select 大致结构

1
2
3
4
5
6
7
8
select {
case <-chan1:
//如果chan1成功读到数据,则进入本条case语句
case chan2 <- 111:
//如果成功向chan2写入数据,则进入本条case语句
default:
//如果上面都没有成功,则进入default语句
}

6.1.1 注意事项1

写入数据成功指的是数据写入了,并且有读取了它
向管道中写入数据未必能够写成功!向管道中写入数据,那么就必须要有个地方可以拿取它。有写入,且有个地方读取了,那么才算是写入成功!
反面案例:写入了,只是表明有写的这个操作,而不代表写成功的结果!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {

ch := make(chan int)

go func() {
fmt.Println("this is a sub-goroutine")
ch <- 666
}()

// 为了不让主协程退出,特意写了一个死循环放在这里
for {
}
}

/*
运行结果:
this is a sub-goroutine

Process finished with exit code 2
*/

下例是写成功的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {

ch := make(chan int)

go func() {
fmt.Println("this is a sub-goroutine")
ch <- 666
}()

value := <-ch
fmt.Println("get value from ch, value=", value)
}

/*
运行结果:
this is a sub-goroutine
get value from ch, value= 666
*/

6.1.2 注意事项2

1.select 中的每个 case 语句里必须是一个 IO 操作。
2.不能switch 语句中的 case 那样写逻辑运算符,比如 < > == !=

6.2 使用 select 实现斐波那契数列

使用生产者消费者模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 生产者函数,不停地生产数字,并放入管道中去
// ch管道只写,quit管道只读
func fibo(ch chan<- int, quit <-chan bool) {
x, y := 1, 1
for {
select {

// 这里,生产者会一直生产数字。
// 当消费者未消费满10个的时候。quit管道是空的,不会接收到数据,由于是无缓冲通道,下面case语句的quit管道就会发生阻塞。Golang内部选择本条case语句。
// 当消费者消费满了10个后,消费者那边就不再从管道里取数字了。由于是无缓冲通道,消费者那边不取数字,这里把数字放入管道的时候就会发生阻塞。
// 这里发送阻塞的时候,由于下面一条case收到了quit管道中放入了数据这一个信息,而且也从quit管道中去读取数据了。
// 那边放入,这里取出,完成了写入成功。下面一条的case就不再阻塞了。
// 这条case阻塞,下面那条case不阻塞,内部就会立马选择下面那条case语句。
case ch <- x: // x的值放入管道中
x, y = y, x+y

// 消费者未消费满10个之前,quit管道是空的。只要通道是空的,没有数据,这里就会阻塞。Golang内部会选择上面那条case语句。
// 当消费满10个了,消费者那边会往quit管道中放入一个值。 一旦放入了值,Golang内部就能立马检测到有数据发送过来了,而且这里也执行了取数据的操作。
// 由于消费满10个之后,消费者那边不取值了,因为管道没有缓冲区,上面那条case语句就会阻塞。
// 上面的case阻塞,这里的case即有数据放入也有取值操作,所以不再阻塞了,Golang内部选择本条case语句。
case flag := <-quit:
fmt.Println("got quit command. flag=", flag)
return // 终止整个函数
}
}
}

func main() {
ch := make(chan int) // 数字通信的管道
quit := make(chan bool) // 标记是否退出

// 生产者,产生数字并放入管道中,同时检测是否有退出指令写入
// 新建一个子协程
go fibo(ch, quit)

// 消费者匿名函数,从管道中读取数字
// 让主协程让处理这个消费者的任务
func() {
// 我只要消费10个就够了
for i := 0; i < 10; i++ {
num := <-ch // ch<-x 数据放入管道的时候,这里没有读出来的时候,还是会阻塞在这里。只是 CPU 执行速度实在太快,一放一读几乎瞬间完成,人类的肉眼看不出阻塞的现象
fmt.Println(num)
}
quit <- true // 消费完后,给 quit 管道写入数据
}()
}

/*
运行结果:
1
1
2
3
5
8
13
21
34
55
got quit command. flag= true
*/

6.3 select实现超时机制

有时候会出现 goroutine 阻塞的情况,如何避免整个程序陷入阻塞的情况?可以利用 select 来设置延时。
比如登陆网银后,一段时间不操作,就自动退出登陆状态了,这就是超时机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
func main() {
fmt.Println("current time:", time.Now())

ch := make(chan int) // 无缓冲的双向管道,可读可写
quit := make(chan bool) // 是否已退出的标记

// 新建一个子协程
go func() {
// 不停地检测两个管道中的数据流动放向
for {
select {

// 另外一个子协程每隔1秒往ch管道中放入数据,一旦放入数据,Golang内部就能立马检测到这一信息,这里便能读取出数据。
// 能读取出数据,数据就是在流动的,这里就不会阻塞。
case num := <-ch:
fmt.Println("num=", num)

// 3 秒后,定时器触发事件。在此之前,定时器没有触发,都是处于阻塞状态。
// 5 个放完后就不再往ch管道中放数据了,而是给quit管道放入了一个数据。此时上面case接收不到数据,上面的case就会阻塞。
// 上面一直阻塞着,阻塞了3秒钟后,定时器被触发了,打印信息,并往quit管道中发送数据。
case <-time.After(3 * time.Second): // 和Sleep()是同时在发生的
fmt.Println("已超时.")
quit <- true
}
}
}()

// 新建另一个子协程,用来往ch管道中放入数据
go func() {
// 每隔 1 秒,往 ch 管道中放入数据
for i := 0; i < 5; i++ {
ch <- i
time.Sleep(time.Second)
}
fmt.Println("sub-goroutine, loop finished. current time:", time.Now())
}()

// 主协程在没有数据放入quit管道之前,就阻塞在这里。
// 一旦有数据放入管道中,Golang检测到了,并读取到了,这里才不会阻塞。
q := <-quit
fmt.Println("current time:", time.Now())
fmt.Println("got quit command, system quit. quit=", q)
}

/*
运行结果:
current time: 2020-02-23 10:53:12.2591937 +0800 CST m=+0.002025201
num= 0
num= 1
num= 2
num= 3
num= 4
sub-goroutine, loop finished. current time: 2020-02-23 10:53:17.2709789 +0800 CST m=+5.013810401
已超时.
current time: 2020-02-23 10:53:19.2717308 +0800 CST m=+7.014562301
got quit command, system quit. quit= true

Process finished with exit code 0
*/

总共用了7秒是因为:当最后一次往 ch 管道放入数据后(放入后子协程会去执行睡1秒的操作),<-ch 接收完了这个数据,就再也接收不到其他任何数据了,就处于阻塞的状态了。5次for循环确实是用了5秒,当子协程在睡1秒的同时,定时器也开始从3秒倒计时了,此时倒计时3秒和睡1秒是同时在发生的事情。睡了1秒之后,定时器也只剩下了2秒。总耗时:5秒+2秒>7秒,因为程序执行也是需要耗费时间的。
注:Golang 的 time.Now() 是以纳秒(十亿分之一秒)来呈现。

6.4 使用 select 输出几个偶数

```go
func main() {
ch := make(chan int, 1) // 缓冲区大小为1,要么是空的,要么是满的

for i := 0; i < 10; i++ {
    select {
    case x := <-ch: // 缓冲区中有数据了,才会选择这个分支
        fmt.Printf("%d ", x)
    case ch <- i: // 缓冲区空的时候,才会选择这个分支
        // do nothing
    }
}

}

/*
运行结果:
0 2 4 6 8
*/