Go 学习笔记(十四)defer、panic 和 recover

本文原创地址:博客园骏马金龙Go 基础系列:defer、panic 和 recover

defer 关键字

defer 关键字可以让 函数或语句 延迟到函数语句块的最结尾时,即即将退出函数时执行,即便函数中途报错结束、即便已经 panic()、即便函数已经 return 了,也都会执行 defer 所推迟的对象。

例如:

func main() {
	a()
}

func a() {
	println("in a")
	defer b()
	println("leaving a")
	//到了这里才会执行b()
}

func b() {
	println("in b")
	println("leaving b")
}

上面将输出:

in a
leaving a
in b
leaving b

即便是函数已经报错,或函数已经 return 返回,defer 的对象也会在函数退出前的最后一刻执行。

func a() TYPE{
	...CODE...
	
	defer b()
	
	...CODE...
	
	// 函数执行出了错误
	
	return args
	// 函数b()都会在这里执行
}

但注意,由于 Go 的作用域采用的是词法作用域,defer 的定义位置决定了它推迟对象能看见的变量值,而不是推迟对象被调用时所能看见的值。

例如:

package main

var x = 10
func main() {
	a()
}

func a() {
	println("start a:",x)   // 输出10
	x = 20
	defer b(x)
	x = 30
	println("leaving a:",x)  // 输出30
	// 调用defer延迟的对象b(),输出20
}

func b(x int) {
	println("start b:",x)
}

比较下面的 defer:

package main

var x = 10

func main() {
	a()
}

func a() int {
	println("start a:", x) // 输出10
	x = 20
	defer func() {
		println("in defer:", x)  // 输出30
	}()
	x = 30
	println("leaving a:", x) // 输出30
	return x
}

上面 defer 推迟的匿名函数输出的值是 30,它看见的不应该是 20 吗?先再改成下面的:

package main

var x = 10

func main() {
	a()
}

func a() int {
	println("start a:", x) // 输出10
	x = 20
	defer func(x int) {
		println("in defer:", x)  // 输出20
	}(x)
	x = 30
	println("leaving a:", x) // 输出30
	return x
}

这个 defer 推迟的对象中看见的却是 20,这和第一种defer b(x)是相同的。

原因在于 defer 推迟的如果是函数,它直接就在它的定义位置处评估好参数、变量。该拷贝传值的的拷贝传值,该指针所见的指针所见 。所以,对于第 (1) 和第 (3) 种情况,在 defer 的定义位置处,就将 x=20 拷贝给了推迟的函数参数,所以函数内部操作的一直是 x 的副本。而第二种情况则是直接指向它所看见的 x=20 那个变量,则个变量是全局变量,当执行 x=30 的时候会将其值修改,到执行 defer 推迟的对象时,它指向的 x 的值已经是修改过的。

再看下面这个例子,将 defer 放进一个语句块中,并在这个语句块中新声明一个同名变量 x:

func a() int {
	println("start a:", x) // 输出10
	x = 20
	{
		x := 40
		defer func() {
			println("in defer:", x)  // 输出40
		}()
	}
	x = 30
	println("leaving a:", x) // 输出30
	return x
}

上面的 defer 定义在语句块中,它能看见的 x 是语句块中x=40,它的 x 指向的是语句块中的 x。另一方面,当语句块结束时,x=40的 x 会消失,但由于 defer 的函数中仍有 x 指向 40 这个值,所以 40 这个值仍被 defer 的函数引用着,它直到 defer 执行完之后才会被 GC 回收。所以 defer 的函数在执行的时候,仍然会输出 40。

如果语句块内有多个 defer,则 defer 的对象以 LIFO(last in first out) 的方式执行,也就是说,先定义的 defer 后执行。

func main() {
	println("start...")
	defer println("1")
	defer println("2")
	defer println("3")
	defer println("4")
	println("end...")
}

将输出:

start...
end...
4
3
2
1

defer 有什么用呢?一般用来做善后操作,例如清理垃圾、释放资源,无论是否报错都执行 defer 对象。另一方面,defer 可以让这些善后操作的语句和开始语句放在一起,无论在可读性上还是安全性上都很有改善,毕竟写完开始语句就可以直接写 defer 语句,永远也不会忘记关闭、善后等操作。

例如,打开文件,关闭文件的操作写在一起:

open()
defer file.Close()
... 操作文件 ...

以下是 defer 的一些常用场景:

  • 打开关闭文件
  • 锁定、释放锁
  • 建立连接、释放连接
  • 作为结尾输出结尾信息
  • 清理垃圾 (如临时文件)

panic()和 recover()

panic() 用于产生错误信息并终止 当前的 goroutine ,一般将其看作是退出 panic()所在函数以及退出调用 panic() 所在函数的函数。例如,G()中调用 F(),F()中调用 panic(),则 F()退出,G() 也退出。

注意,defer 关键字推迟的对象是函数最后调用的,即使出现了 panic 也会调用 defer 推迟的对象。

例如,下面的代码中,main() 中输出一个start main之后调用 a(),它会输出start a,然后就 panic 了,panic() 会输出panic: panic in a,然后报错,终止程序。

func main() {
	println("start main")
	a()
	println("end main")
}

func a() {
	println("start a")
	panic("panic in a")
	println("end a")
}

执行结果如下:

start main
start a
panic: panic in a

goroutine 1 [running]:
main.a()
		E:/learning/err.go:14 +0x63
main.main()
		E:/learning/err.go:8 +0x4c
exit status 2

注意上面的end aend main都没有被输出。

可以使用 recover()去捕获 panic()并恢复执行。recover()用于捕捉 panic()错误,并返回这个错误信息。但注意,即使 recover()捕获到了 panic(),但调用含有 panic()函数的函数 (即上面的 G() 函数)也会退出,所以如果 recover()定义在 G()中,则 G()中调用 F()函数之后的代码都不会执行(见下面的通用格式)。

以下是比较通用的 panic()和 recover() 的格式:

func main() {
	G()
	// 下面的代码会执行
	...CODE IN MAIN...
}
func G(){
	defer func (){
		if str := recover(); str != nil {
			fmt.Println(str)
		}
	}()
	...CODE IN G()...
	
	// F()的调用必须在defer关键字之后
	F()
	// 该函数内下面的代码不会执行
	...CODE IN G()...
}
func F() {
	...CODE1...
	panic("error found")
	// 下面的代码不会执行
	...CODE IN F()...
}

可以使用 recover()去捕获 panic() 并恢复执行。但以下代码是错误的:

func main() {
	println("start main")
	a()
	println("end main")
}

func a() {
	println("start a")
	panic("panic in a")

	// 直接放在panic后是错误的
	panic_str := recover()
	println(panic_str)

	println("end a")
}

之所以错误,是因为 panic()一出现就直接退出函数 a() 和 main()了。要想 recover() 真正捕获 panic(),需要将 recover() 放在 defer 的推迟对象中,且 defer 的定义必须在 panic() 发生之前。

例如,下面是通用格式的示例:

package main

import "fmt"

func main() {
	println("start main")
	b()
	println("end main")
}

func a() {
	println("start a")
	panic("panic in a")
	println("end a")
}

func b() {
	println("start b")
	defer func() {
		if str := recover(); str != nil {
			fmt.Println(str)
		}
	}()
	a()
	println("end b")
}

以下是输出结果:

start main
start b
start a
panic in a
end main

注意上面的end bend a都没有被输出,但是end main输出了。

panic()是内置的函数 ( 在包 builtin 中),在log包中也有一个 Panic()函数,它调用 Print() 输出信息后,再调用 panic()。go doc log Panic一看便知:

$ go doc log Panic
func Panic(v ...interface{})
	Panic is equivalent to Print() followed by a call to panic().

上一篇 Go 学习笔记(十三)函数 (1)
Go 学习笔记(目录)
下一篇 Go 学习笔记(十五)函数 (2)——回调函数和闭包