Defer, Panic, and Recover

前端之家收集整理的这篇文章主要介绍了Defer, Panic, and Recover前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

工作之后两年多没写过博客了,给自己找了一个忙的借口,快过年了,有点时间,最近学习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

猜你在找的Go相关文章