Golang defer、panic和recover

Posted by 淦 Blog on January 26, 2023

defer

defer是 Go语言中的关键字,也是Go语言的重要特性之一。defer 的语法形式为 defer Expression,其后必须紧跟一个函数调用或者方法调用。在很多时候,defer后的函数以匿名函数或闭包的形式呈现,例如:

1
2
3
defer func(...){
//实际处理
}()

defer将其后的函数推迟到了其所在函数返回之前执行。例如在运行如下代码后,将首先打印出下方的”normal func”,接着打印出”defer func”。

1
2
3
4
func main() {
	defer fmt.Println("defer func")
	fmt.Println("normal func")
}

不管defer函数后的执行路径如何,最终都将被执行。在Go语言中,defer一般被用于资源的释放及异常panic的处理。

程序中可以有多个 defer、defer的调用可以存在于函数的任何位置等。defer可能不会被执行,例如,如果判断条件不成立则放置在if语句中的 defer可能不会被执行。

使用

资源释放

不管defer后面的执行路径如何,defer中的语句都将执行。在每个资源后都立即加入defer file.Close函数,保证函数在任意路径执行结束后都能够关闭资源。defer是一种优雅的关闭资源的方式,能减少大量元余的代码并避免由于忘记释放资源而产生的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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的特性是无论后续函数的执行路径如何以及是否发生了panic,在函数结束后一定会得到执行,这为异常捕获提供了很好的时机。异常捕获通常结合recover函数一起使用。

如下所示,当在defer函数中使用recover进行异常捕获后,程序将不会异常退出,并且能够执行正常的函数流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func executePanic() {
	defer func() {
		if errMsg := recover(); errMsg != nil {
			fmt.Println(errMsg)
			fmt.Println("This is recovery function... ")
		}
	}()
	panic("This is Panic Situation")
	fmt.Println("The function executes Completely")
}

func main() {
	executePanic()
	fmt.Println("Main block is executed completely...")
}

如下输出表明,尽管有panic,main函数仍然在正常执行后退出。

1
2
3
This is Panic Situation
This is recovery function...
Main block is executed completely...

特性

延迟执行

defer后的函数并不会立即执行,而是推迟到了函数结束之后执行,这一特性一般用于资源的释放和异常捕获。

参数预计算

当函数到达defer语句时,延迟调用的参数将立即求值,传递到defer函数中的参数将预先被固定,而不会等到函数执行完成后再传递参数到defer中。

如下例所示,defer 后的函数需要传递int 参数,首先将a赋值为1,接着defer函数的参数传递为a+1,最后,在函数返回前a被赋值为99。那么最后defer函数打印出的b值是多少呢?答案是2。原因是传递到defer的参数是预执行的,因此在执行到defer语句时,执行了a+1并将其保留了起来,直到函数执行完成后才执行defer函数体内的语句。

1
2
3
4
5
6
7
func main() {
	a := 1
	defer func(b int) {
		fmt.Println("defer b", b)
	}(a + 1)
	a = 99
}

defer多次执行与LIFO执行顺序

在函数体内部,可能出现多个defer函数。这些defer函数将按照后入先出(last-in-first-out,LIFO)的顺序执行,这与栈的执行顺序是相同的,这也意味着后申请的资源将会先得到释放。

返回值陷阱

1
2
3
4
5
6
func main() {
	i := 0
	defer fmt.Println(i)
	i++
	return
}

程序输出结果如下:

1
1
1
2
3
4
5
6
7
8
9
10
11
func c() (i int) {
	defer func() {
		i++
	}()
    // i = 1
	return 1
}

func main() {
	fmt.Print(c())
}

程序输出结果如下:

1
2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var g = 100

func f() (r int) {
	defer func() {
		g = 200
	}()
	fmt.Printf("f: g = %d\n", g)
    // r = g
	return g
}

func main() {
	i := f()
	fmt.Printf("main: i = %d, g = %d\n", i, g)
}

程序输出结果如下:

1
2
f: g = 100
main: i = 100, g = 200

从输出结果可以推测出,在return之后,执行了defer函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var g = 100

func f() (r int) {
	r = g
	defer func() {
		r = 200
	}()
	r = 0
	return r
}

func main() {
	i := f()
	fmt.Printf("main: i = %d, g = %d\n", i, g)
}

程序输出结果如下:

1
main: i = 200, g = 100

总结:将返回值保存在栈上->执行defer函数->函数返回。

panic

1
func panic(interface{})

panic函数传递的参数为空接口interface{},其可以存储任何形式的错误信息并进行传递。在异常退出时会打印出来。

Go程序在panic时并不会像大多数人想象的一样导致程序异常退出,而是会终止当前函数的正常执行,执行defer 函数并逐级返回。例如,对于函数调用链a()->b()->c(),当函数c发生panic后,会返回函数b。此时,函数b也像发生了panic一样,返回函数a。在函数c、b、a中的defer函数都将正常执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func a() {
	defer fmt.Println("defer a")
	b()
	fmt.Println("after a")
}

func b() {
	defer fmt.Println("defer b")
	c()
	fmt.Println("after b")
}

func c() {
	defer fmt.Println("defer c")
	panic("this is a panic")
	fmt.Println("after c")
}

func main() {
	a()
}

如下所示,当函数c触发了panic后,所有函数中的defer语句都将被正常调用,并且在panic 时打印出堆栈信息。

1
2
3
4
5
6
defer c
defer b
defer a
panic: this is a panic
goroutine 1 [running]:
main.c()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func a() {
	defer b()
	panic("a panic")
}

func b() {
	defer c()
	panic("b panic")
}

func c() {
	panic("c panic")
}

func main() {
	a()
}

程序输出结果如下:

1
2
3
panic: a panic
panic: b panic
panic: c panic

先打印最早出现的panic,在打印其他的panic。

recover

1
func recover() interface{}

为了让程序在panic时仍然能够执行后续的流程,Go语言提供了内置的recover函数用于异常恢复。recover函数一般与defer函数结合使用才有意义,其返回值是panic中传递的参数。由于panic会调用defer函数,因此,在defer函数中可以加入rover起到让函数恢复正常执行的作用。

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
func a() {
	defer fmt.Println("defer a")
	b()
	fmt.Println("after a")
}

func b() {
	defer func() {
		fmt.Println("defer after b")
		if x := recover(); x != nil {
			fmt.Printf("run time panic: %v\n", x)
		}
	}()
	c()
	fmt.Println("after b")
}

func c() {
	defer fmt.Println("defer c")
	panic("this is a panic")
	fmt.Println("after c")
}

func main() {
	a()
}

程序输出结果如下:

1
2
3
4
5
defer c
defer after b
run time panic: this is a panic
after a
defer a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func a() {
	defer b()
	panic("a panic")
}

func b() {
	defer c()
	panic("b panic")
}

func c() {
	panic("c panic")
}

func catch(funcname string) {
	if r := recover(); r != nil {
		fmt.Println(funcname, "recover:", r)
	}
}

func main() {
	defer catch("main")
	a()
}

程序输出结果如下:

1
main recover: c panic

recover函数最终捕获的是最近发生的panic,即便有多个panic函数,在最上层的函数也只需要一个recover函数就能让函数按照正常的流程执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func a() {
	defer b()
	panic("a panic")
}

func b() {
	defer c()
	panic("b panic")
}

func c() {
	defer catch("c")
	panic("c panic")
}

func catch(funcname string) {
	if r := recover(); r != nil {
		fmt.Println(funcname, "recover:", r)
	}
}

func main() {
	a()
}

程序输出结果如下:

1
2
3
4
5
c recover: c panic
panic: a panic
panic: b panic
goroutine 1 [running]:
main.b()

内部的recover只能捕获由当前函数或其子函数触发的panic,而不能触发上层的panic。