- 原文标题:Defer, Panic, and Recover
- 原文做者:Andrew Gerrand
- 原文时间:2010-08-04
Go 语言有通用的控制流机制:if
, for
, switch
, goto
。还有 go
语句将代码运行在不一样的 goroutine 中。这里我要讨论下几个不是那么经常使用的控制流:defer
, panic
和 recover
。golang
一个 defer 语句会将一个函数调用加到特定的列表中。在某个函数返回的时候,这个列表中存储的函数将被执行。Defer 一般用于简化那些有不少清理动做的函数。
例如,下面这个函数打开了两个文件,而后将其中一个文件的内容复制到另外一个中:编程
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != null {
return
}
dst, err := os.Create(dstName)
if err != nil {
return
}
written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}
复制代码
这样写能正常运行,可是有个 bug。若是函数 os.create
调用失败,整个函数将直接返回,而源文件却没有关闭。修复这个 bug 很是简单,只要在第二个 return 语句前加上 src.Close
便可。但若是这个函数变得很是复杂,那这个 bug 就很难被发现和解决。经过引入 defer 语言,咱们老是能保证打开的文件能关闭:json
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != null {
return
}
defer src.Close()
des, err := os.Create(dstName)
if err != nil {
return
}
def dst.Close()
return io.Copy(dst, src)
}
复制代码
Defer 语句能让咱们在打开一个文件以后立刻考虑关闭的状况,并且是有保证的,函数中不管有多少个 return 语句,打开的文件总能关闭。
Defer 语句的行为是很是简单和可预测的,知足如下三条简单规则:数组
下面例子中,表达式i的值是在 defer 声明出肯定的。函数返回后,defer 语句输出为:0。bash
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
复制代码
下面函数输出是:3210编程语言
func b() {
for i:=0; i < 4; i++ {
defer fmt.print(i)
}
}
复制代码
下面例子中,函数返回后,defer 声明的函数改变了返回变量 i 的值。所以下面函数返回值为:2函数
func c() (i int) {
defer func() { i++ }()
return 1
}
复制代码
经过这个特性,能方便的修改函数的错误返回。后面咱们会看到一个简单的例子。ui
Panic 是一个内建 (bulit-in) 函数,能中止函数正常的控制流并进入 panicking 状态。但某个函数F调用了 panic 函数,函数F中止执行,F中声明的 defer 函数开始执行,以后返回到F的调用者。对于调用者来讲,调用F函数就如同调用了 panic 函数。该过程继续向上移动,直到当前goroutine 中的全部函数都返回,此时程序崩溃。Panics 可能源于 panic 函数调用,也多是运行时错误,好比数组越界。spa
Recover 是一个内建函数,能将状态为 panicking 的 goroutine 恢复到正常的控制流。Recover 只能在声明为 defer 的函数才有用。正常状况下调用 recover 函数将返回 nil 且没有任何效果。若是当前 goroutine 为 panicking 状态,调用 recover 函数将捕获 panic 的值而且回到正常控制流。
下面例子演示 panic 和 defer 机制:code
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)
}
复制代码
函数g接受一个参数 int i
,若是 i 大于 3 则抛出 panic,不然参数加 1,再次调用本身。函数f defer 了一个函数,这个函数 recover 了 panic 而后输出收到的值(若是不为 nil )。如今请读者停下来,试着分析下上面程序的输出。
这个程序的输出:
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 将不会被捕获,直到到达当前 goroutine 的调用栈顶,程序终止。修改后的输出为:
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]
复制代码
真实使用 panic 和 recover 的状况,能够查看 Go 标准库中 json package 的实现。它使用一些列递归函数来解析 JSON。但遇到非法 JSON 时,解析器抛出 panic,函数栈上面的函数将会捕获这个 panic,返回一个适当的错误值。(查看 'error' 和 'unmarshal' 方法关于解码状态在decode.go中)
Go 库中的约定是即便包在内部使用 panic,其外部 API 仍然会显示明确的错误返回值。
defer 的其余的用法包括释放互斥锁(mutex):
mu.Lock()
defer mu.Unlock()
复制代码
打印结尾:
printHeader()
defer printFooter()
复制代码
固然还有跟多其余的用法。
总的来讲,defer 语句(不管有没有 panic 或 recover )提供了一个通用和强大的流控制机制。它可用于模拟由其余编程语言中的专用结构实现的许多功能。本身试试看。
// 原始 defer 基于函数,下面 defer 一个无参数和返回值的匿名函数,并当即调用
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 f() {
defer {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}
...
}
复制代码