美文网首页程序员
Defer, Panic, Recover

Defer, Panic, Recover

作者: 沈渊 | 来源:发表于2018-12-08 23:19 被阅读13次

1、简介

Go具有控制流程的常用机制:if,for,switch,goto。 它还有go语句在单独的goroutine中运行代码。 在这里,我想讨论一些不太常见的问题:Defer,Panic和Recover。

2、Defer

Defer语句将函数调用推送到列表中。 周围函数返回后执行已保存调用的列表。 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
}

这样确实能正常运行,但有一个错误。 如果对os.Create的调用失败,该函数将返回而不关闭源文件。 这可以通过在第二个return语句之前调用src.Close来轻松解决,但如果函数更复杂,则问题可能不会那么容易被注意到并解决。 通过引入Defer语句,我们可以确保文件始终关闭:

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语句的行为是直截了当且可预测的。 有三个简单的规则:

2.1、在计算defer语句时,将计算延迟函数的参数。

在此示例中,在延迟Println调用时计算表达式“i”。 函数返回后,延迟调用将打印“0”。

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

2.2、在周围函数返回后,延迟函数调用以Last In First Out顺序执行。

如下函数将打印“3210”:

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}

2.3、Defer函数可以读取并分配给返回函数的命名返回值。

在如下示例中,Defer函数在周围函数返回后递增返回值i。 因此,此函数最终返回 2:

func c() (i int) {
    defer func() { i++ }()
    return 1
}

这样便于修改函数的错误返回值;

3、Panic 和 Recover

Panic是一个内置函数,可以阻止普通的控制流。 当函数F调用panic时,F的执行停止,F中的任何Defer函数都正常执行,然后将F返回其调用者。 对于调用者,只会感受F为Panic。 该过程继续向上移动,直到当前goroutine中的所有函数都返回,此时 程序崩溃。 可以通过直接调用 panic 来启动 panic。 它们也可能由运行时错误引起,例如越界数组访问。
Recover是一个内置函数,可以重新控制 panic 的goroutine。 recover仅在defer函数内有用。 在正常执行期间,对recover的调用将返回nil并且没有其他效果。 如果当前goroutine处于 panic 状态,则对 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并打印恢复值的函数(如果它是非零的)。 尝试在阅读之前描绘该程序的输出。
该程序将输出:

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包。 它使用一组递归函数对JSON编码的数据进行解码。 当遇到格式错误的JSON时,解析器调用panic将堆栈展开到顶级函数调用,该函数调用从panic中恢复并返回适当的错误值(请参阅 decode.go 中的decodeState类型的'error'和'unmarshal'方法)。
Go 库的原则是即使在包的内部使用了 panic,在它的对外接口(API)中也必须用 recover 处理成返回显式的错误。

4、其他

4.1、Defer类似Java中finally

使用过程中,defer类似Java中finally,即使panic(即java中 throw exception),依然能够执行

4.2、panic 只能在本 goroutine 处理

若尝试在main中recover goroutine中panic,将无法达到预期,程序仍然会结束

4.3、recover 只能在 defer 中有效

golang的要求,recover只能写在defer中

4.4、多使用recover除占用cpu外,不会影响服务正常

如果函数没有 panic,调用 recover 函数不会获取到任何信息,也不会影响当前进程。

5、参考文献

  1. Defer, Panic, and Recover
  2. Golang: 深入理解panic and recover

相关文章

网友评论

    本文标题:Defer, Panic, Recover

    本文链接:https://www.haomeiwen.com/subject/erychqtx.html