go语言学习 - 协程、延迟函数以及恐慌和恢复

golang

协程(goroutine)

协程是go语言内部唯一实现程序并发的方式,go不支持创建系统线程

在Go中,开启一个新的协程是非常简单的。 我们只需在一个函数调用之前使用一个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
// 在下面的例子程序中,主协程创建了两个新的协程,由于主协程Sleep 2秒。所以两个协程内容没有执行完。
package main

import (
"log"
"math/rand"
"time"
)

func SayGreetings(greeting string, times int) {
for i := 0; i < times; i++ {
log.Println(greeting)
d := time.Second * time.Duration(rand.Intn(5)) / 2
time.Sleep(d)
}
}

func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)
go SayGreetings("hi!", 10)
go SayGreetings("Hello!", 10)
time.Sleep(2 * time.Second)
}

并发同步

使用通道技术可以实现并发同步,sync标准库提供的WaitGroup可以很简单实现这一点

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
package main

import (
"log"
"math/rand"
"runtime"
"sync"
"time"
)

var wg sync.WaitGroup

func SayGreetings(greeting string, times int) {
for i := 0; i < times; i++ {
log.Println(greeting)
d := time.Second * time.Duration(rand.Intn(5)) / 2
time.Sleep(d)
}
// 协程完成时候通知任务已完成
wg.Done()
}

func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)
log.Println(runtime.NumCPU())
// 用add方法注册两个新任务
wg.Add(2)
go SayGreetings("hi v!", 10)
go SayGreetings("hello z!", 10)
// 阻塞,直到任务都完成
wg.Wait()
// 当上面协程都完成,此时才会Sleep
time.Sleep(2 * time.Second)
log.Println("end")
}

延迟函数

一个在defer后面的函数调用就是延迟函数,多个延迟函数遵守后进先出规则执行

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
// 下面例子,最后打印值为0~9
package main

import "fmt"

func main() {
defer fmt.Println("9")
fmt.Println("0")
defer fmt.Println("8")
fmt.Println("1")
if false {
defer fmt.Println("not reachable")
}
defer func() {
defer fmt.Println("7")
fmt.Println("3")
defer func() {
fmt.Println("5")
fmt.Println("6")
}()
fmt.Println("4")
}()
fmt.Println("2")
return
defer fmt.Println("not reachable")
}

修改函数返回值

一个延迟调用可以修改包含此延迟调用的最内层函数的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 下面例子,先执行r = n + n,r = 10。然后执行延迟函数 r = r +n,此时r = 10,n = 5,最后返回值15
package main

import "fmt"

func Triple(n int) (r int) {
defer func() {
r += n // 修改返回值
}()

return n + n // <=> r = n + n; return
}

func main() {
fmt.Println(Triple(5)) // 15
}

协程和延迟调用的实参的估值时刻

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 第一个匿名函数中的循环打印出2、1和0这个序列,但是第二个匿名函数中的循环打印出三个3。 因为第一个循环中的i是在fmt.Println函数调用被推入延迟调用队列的时候估的值,而第二个循环中的i是在第二个匿名函数调用的退出阶段估的值(此时循环变量i的值已经变为3)。
package main

import "fmt"

func main() {
func() {
for i := 0; i < 3; i++ {
defer fmt.Println("a:", i)
}
}()
fmt.Println()
func() {
for i := 0; i < 3; i++ {
defer func() {
fmt.Println("b:", i)
}()
}
}()
}

可以对第二个循环修改一下,使得它的结果和第一个一样

1
2
3
4
5
6
for i := 0; i < 3; i++ {
defer func(i int) {
// 此i为形参i,非实参循环变量i。
fmt.Println("b:", i)
}(i)
}

同样的估值时刻规则也适用于协程调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 下面这个例子程序将打印出123 789
package main

import "fmt"
import "time"

func main() {
var a = 123
go func(x int) {
time.Sleep(time.Second)
fmt.Println(x, a) // 123 789
}(a)

a = 789

time.Sleep(2 * time.Second)
}

恐慌和恢复

Go不支持异常抛出和捕获,而是推荐使用返回值显式返回错误。 不过,Go支持一套和异常抛出/捕获类似的机制。此机制称为恐慌/恢复(panic/recover)机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
defer func() {
recover()
fmt.Println("panic is over")
}()
panic("panic is coming")
fmt.Println("end?")
}