工作之后两年多没写过博客了,给自己找了一个忙的借口,快过年了,有点时间,最近学习go语言,习惯于学习官方文档,发现
https://blog.golang.org/defer-panic-and-recover
这篇文章没有翻译版本,可能会延长学习的时间成本,就翻译了一下,翻译的不妥请大家指正
Defer,Panic,and Recover
Go语言有着通用的流程控制机制:if, for, switch,goto。同样有在独立go程中运行代码的机制。我们这里讨论一个相对于前两者不那么常用的机制:defer, panic和recover。
defer表达式将函数调用压进一个线性表中(理解为堆栈)。在所有上层函数返回后(即当前层次调用的所有函数返回后,并且当前函数调用return),线性表中的调用开始执行。defer一般被用来简化需要进行一些清理操作的函数。
举个例子,我们来看一个执行文件内容拷贝操作的函数,即打开两个文件,并将其中一个文件的内容拷贝到另一个文件中
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 }
这个函数可以执行,但是有个bug,如果os.Create调用失败,那么函数将会返回,但是不会close源文件。这个问题只要在第二个return语句之前补一个src.Close就可以修复。但是如果这种情况发生在一个复杂逻辑中,这一问题可能并没有这么容易被发现并修复。通过引入defer表达式,我们可以保证文件close总是被调用:
func CopyFile(dstName,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语句允许我们在刚刚打开文件的时候就考虑如何关闭文件。无论函数有多少return语句,只要保证这一点,文件就会被close。
defer语句的行为是直接并且可以预测的,有三个简单的原则:
1.defer语句一旦运行,那么被执行defer操作的函数的参数值就被确定,举个例子:
func a() { i := 0 defer fmt.Println(i) i++ return }
在这个例子中,变量i在Println被defer调用时候就被计算出来,因此在函数返回后,被defer的调用会打印0而不是1
2. 在外层函数返回后,被defer的函数按照后进先出(LIFO,因此我理解为堆栈)的顺序执行,比如下面函数最终打印“3210”:
func b() { for i := 0; i < 4; i++ { defer fmt.Print(i) } }
3. 被defer的函数可能读取并且赋值给正在返回函数的具备名称的返回值。
func c() (i int) { defer func() { i++ }() return 1 }
在这个例子中, 一个defer的函数在外层函数返回后对返回值i执行自增操作,因此,这个函数返回后i的值为2.这有利于修复函数的错误返回值,稍后将会看到一个例子。
panic是一个结束正常的控制流程,并且启动panicking(不知道怎么翻)机制的内建方法。当函数F调用panic时,F的执行结束,所有F中被defer的函数开始执行,然后F返回到调用者。对于调用者而言,F接下来的行为像一个对panic的调用。进程持续退栈操作直到当前go程的多有方法返回,在这一点程序失败。panic可以通过直接引入panic状态开始。他们可以被运行时错误导致,例如数组的越界访问。
recover是一个恢复对panicking状态go程控制的内建方法。recover只有在被defer的函数或方法中才有效。在正常执行过程中,对recover的调用将会返回nil,不会有其他影响。如果当前go程正处于panicking状态,对recover的调用将会捕捉传入panic的值并且恢复正常执行。
下面是一个用来阐明panic和defer的例程:
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,否则使用i+1递归调用函数自身。函数f对一个调用了recover并且打印被恢复的值(如果部位nil的话)的函数进行的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 Recovered in f 4 Returned normally from f.
如果我们从f中将被defer的函数移除,那么panic就没有被recover,在达到go程调用栈顶端之后,终止程序,输出会如下所示:
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包。它使用一系列的递归和循环函数解析使用json编码的数据。在遇到畸形的json数据时,解析器调用panic释放栈空间直到最高一级的函数调用,这一函数调用从panic恢复并且返回一个合理的error 值(详见 decode.go中解码状态类型'error' 和'unmarshal' 方法)
go标准库的约定是,虽然包内部使用了panic,他的外部接口依然要返回的明确error值。
其他defer用法(除了早先提到的file.Close的例子)包括释放一个互斥量
mu.Lock() defer mu.Unlock()
打印页脚
printHeader() defer printFooter()
以及其他的更多使用。
综上所述,defer语句(无论是否和panic与recover一起使用)提供了一套不同寻常但是有力的流程控制机制。这一机制可以用来规范其他语言的一大批使用特殊结构完成的功能,可以尝试一下。
英文版原作者:Andrew Gerrand
原文链接:https://www.f2er.com/go/188972.html