Defer, Panic, and Recover

来源:互联网 发布:股票下载什么软件 编辑:程序博客网 时间:2024/06/05 09:54

工作之后两年多没写过博客了,给自己找了一个忙的借口,快过年了,有点时间,最近学习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, 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语句允许我们在刚刚打开文件的时候就考虑如何关闭文件。无论函数有多少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 mainimport "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 0Printing in g 1Printing in g 2Printing in g 3Panicking!Defer in g 3Defer in g 2Defer in g 1Defer in g 0Recovered in f 4Returned normally from f.


如果我们从f中将被defer的函数移除,那么panic就没有被recover,在达到go程调用栈顶端之后,终止程序,输出会如下所示:
Calling g.Printing in g 0Printing in g 1Printing in g 2Printing in g 3Panicking!Defer in g 3Defer in g 2Defer in g 1Defer in g 0panic: 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


0 0
原创粉丝点击