背景

除了正常的流程控制之外,Go 还提供了 3 种工具来做异常的流程控制:defer,panic 和 recover。

介绍

Defer 的使用

defer 语句其实有点像 C++ 种的析构函数,但是又并不与「对象」这个概念挂钩,它更多的是在目前函数中注册清理清理函数,当函数调用结束时(有可能是异常中止),触发已注册清理函数的执行。

如这个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func CopyFile(dstName, srcName string) (written int64, err error) {
	src, err := os.Open(srcName)
	if err != nil {
		return
	}

	dst, err := os.Create(dstName)
	if err != nil {
		return
	}

	written, err = io.Copy(dst, src)
	dst.Close()
	src.Close()
	return
}

其中,如果 os.Create 动作因为异常中止了,已打开的 src 将不会被正常关闭。为了在函数退出控制流中插入我们的清理函数,可以使用 defer 语句,如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func CopyFile(dstName, srcName string) (written int64, err error) {
	src, err := os.Open(srcName)
	if err != nil {
		return
	}
	defer src.Close()

	dst, err := os.Create(dstName)
	if err != nil {
		return
	}
	defer dst.Close()

	return io.Copy(dst, src)
}

使用 defer 语句有 3 条规则:

1. defer 函数的参数将在定义时被确定

如以下这个例子:

1
2
3
4
5
6
func a() {
	i := 0
	defer fmt.Println(i)  // 函数结束后将会打印 0
	i = 250
	return
}

2. defer 函数的调用遵循 Last In First Out,即栈顺序

如以下这个例子:

1
2
3
4
5
func b() {
	for i := 0; i < 4; i++ {  
		defer fmt.Print(i) // 注册顺序:0123,将打印 3210
	}
}

3. defer 函数可以读取并修改函数的具名返回值

如以下这个例子:

1
2
3
4
func c() (i int) { // 返回一个 int 类型的值,变量名叫 i
	defer func() { i++ }() // 读取 i,并自增
	return 1
}

Panic 的使用

panic 是一个内置函数,用于中止正常的执行流并打印堆栈,期间将触发 defer 函数的调用。当 F() 调用 panic,它将中止 F() 的执行并触发 F() 的 defer 函数,并返回它的上一层调用者。对于上一层调用者,也相当于调用了 panic,中止正常流程并触发 defer 函数,这样一层层回溯直到顶端,此时程序就会中止。

如果发生了不可恢复的错误,可以直接使用 panic 退出程序的运行。 实际上,库函数应该避免 panic。若问题可以被屏蔽或解决,最好就让程序继续运行而不是终止整个程序。 一个可能的反例就是初始化:若某个库真的不能自己工作,且有足够的理由产生 panic,那就由它去吧。

Recover 的使用

recover 是一个内置函数,可用与程序发生 panic 的时候重新获取控制权。

当 panic 被调用后,程序将立刻终止当前函数的执行,并开始回溯 goroutine 的栈,运行 defer 函数。若回溯到达 goroutine 的顶端,程序就会终止。 我们可用内建的 recover 函数来重新取回控制权并使其恢复正常运行。

调用 recover 将停止回溯过程,并返回传入 panic 的实参。由于在回溯中只有 defer 函数中的代码在运行,因此 recover 只能在 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
27
28
29
30
31
package main

import "fmt"

func f() {
	// 当因为调用 g() 而导致发生 panic 的时候,将触发 f() 的 defer 函数
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered in f()", r)
		}
	}()
	fmt.Println("Calling g()")
	g(0)
	fmt.Println("Returned normally from g()")
}

func g(i int) {
	if i > 3 {
		fmt.Println("Panicking")
		panic(fmt.Sprintf("%v", i))
	}
	defer fmt.Println("Defer in g()", i)
	fmt.Println("Printing in g()", i)
	g(i + 1)
}

func main() {
	f()
	fmt.Println("Returned normally from f()")
}

这段代码将打印:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Calling g()
Printing in g() 0
Printing in g() 1
Printing in g() 2
Printing in g() 3
Panicking
Defer in g() 3
Defer in g() 2
Defer in g() 1
Defer in g() 0
Recovered in f() 4
Returned normally from f()

如果 f() 没有使用 recover,将会直接 panic。

参考资料