golang中的defer, panic和recover
Go提供了 defer
, panic
和 recover
三个内置方法。其中 panic
会让程序崩溃,defer
可以在函数 return 之前执行操作, defer
和 recover
配合可以捕获 panic。
defer
defer
声明的语句可以在函数或方法返回(不管是正常返回或异常返回)之前调用,类似于 Java 里面的 finally
,可以做一些清理的工作,比如关闭文件、 释放资源等操作。
程序 1 展示了 defer
的一般的用法,通过 defer
语句保证 src
和 dst
最终会被释放。
// 程序 1
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 语句有三个约定:
defer 语句参数的值在 defer 语句声明时就已经确定了
// 程序 2 func a() { i := 0 defer fmt.Println(i) i++ return }
defer 语句并不是简单的延迟执行,程序 2 中的
a()
方法执行到defer fmt.Println(i)
时,会将i的值 copy 一份和defer语句的声明一起入栈,在 return 之前,声明的 defer 语句出栈执行,所以程序 2 最终打印出i的值是0
。defer 语句的执行顺序是后进先出(LIFO)
// 程序 3 func b() { for i := 0; i < 4; i++ { defer fmt.Print(i) } }
defer 语句的声明和执行可以看作是 defer 语句块的「入栈」和「出栈」操作,先声明的 defer 语句最后执行,所以程序 3 的输出是
3201
defer 语句可以读取并修改外部函数命名的返回值(named return values)
// 程序 4 func c() (i int) { defer func() { i++ }() return i }
defer 语句可以在 return 之前执行, 并且可以修改外部函数命名的返回值(named return values)。程序 4 在 return 之前会执行 defer 语句,所以程序 4 最终返回的是
1
。// 程序 5 func c() int { var i int defer func() { i++ }() return i }
但程序 5 最终输出的是
0
。
panic和recover
panic
内置函数可以让让当前 goroutine 崩溃,当函数 F 中调用了或者触发了 panic
,F 会立即终止运行,然后执行 F 中的 defer
语句,然后 F 返回到调用者 G,G 也会立即终止运行,然后执行 G 中的 defer
语句,这样一层一层的向上返回,直到顶层的 goroutine(函数调用链的顶层 goroutine,不一定是 main goroutine),然后程序崩溃。
recover
内置方法可以再次控制 panic 的 goroutine。recover
方法只有在 defer
中才有效果。正常情况下,recover
会返回 nil
,如果当前 goroutine 发生了 panic,recover
方法会捕获 panic 的值,并且再次获得程序的控制权。
// 程序 6
package main
import "fmt"
func main() {
f()
fmt.Println("Returned normally from f.")
}
func f() {
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)
}
程序 6 中 g 的逻辑是如果 i>3
,则 panic,否则递归进行 i+1
,f 在 defer
中调用了 recover
,并打印了 recover 信息,程序的输出:
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中的 defer
声明,panic 不会 recover,一层一层的返回 panic
,直到 goroutine 调用栈的顶端,然后程序崩溃。去掉 defer 语句之后的输出:
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
panic: 4
panic PC=0x2a9cd8
[stack trace omitted]
recover
返回的是 panic
的值,以下三种情况,recover
的值是 nil
:
panic
的值是nil
goroutine
没有 panicrecover
没有直接在defer
中调用
前两种情况好理解,第三种情况,如果 recover
没有在 defer
中直接调用,那么 recover
就不能捕获 panic
。下面三段代码,程序 7 中的 recover()
可以正常捕获 panic,而程序 8 和程序 9 则会 panic。
// 程序 7
package main
import "fmt"
func main() {
a()
}
func a() {
defer r()
panic("a")
}
func r() {
if r := recover(); r != nil {
fmt.Println(r)
}
}
// 程序 8
package main
import "fmt"
func main() {
a()
}
func a() {
defer func() {
r()
}()
panic("a")
}
func r() {
if r := recover(); r != nil {
fmt.Println(r)
}
}
// 程序 9
package main
import "fmt"
func main() {
a()
}
func a() {
r()
panic("a")
}
func r() {
defer func() {
if r := recover(); r != nil {
fmt.Println(r)
}
}()
}
总结
这篇文章介绍了 golang 内置的 defer、panic 和 recover 函数,我们了解到:
defer
声明时参数就确定了,不会随着方法内部代码的执行而变化。defer
执行顺序是后进先出(LIFO)。defer
可以读取并修改外部函数命名的返回值(named return values)。recover
必须在defer
中,并且是defer
直接调用才能捕获panic
。