Defer, Panic, Recover

综合技术 2018-12-09 阅读原文

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
Go语言中文网

责编内容by:Go语言中文网阅读原文】。感谢您的支持!

您可能感兴趣的

NSQ源码剖析之nsqd NSQ 是实时的分布式消息处理平台,其设计的目的是用来大规模地处理每天数以十亿计级别的消息。 NSQ 具有分布式和去中心化拓扑结构,该结构具有无单点故障、故障容错、高可用性以及能够保证消息的可靠传递的特征。##Topic与Channel...
并发 Go 程序中的共享变量 (一):data race... 本系列是阅读 “The Go Programming Language” 理解和记录。 在 Go 的程序中,如果只有一个 goroutine 也就是只有一个 main goroutine 存在时,所有代码都是顺序执行的,也就是说程序的执...
concurrency in go 读书笔记 《concurrency in go》这本书出版于 2017 年八月,里面有些观点还是蛮新颖的,烂大街的我就先不写了,重点写写书里提到的,我之前忽视的观点,以及一些奇技淫巧。 锁的粒度太大的话,有可能造成其它的 goroutine...
原 荐 Go 并发控制context实现原理剖析 1. 前言 Golang context是Golang应用开发常用的并发控制技术,它与WaitGroup最大的不同点是context对于派生goroutine有更强的控制力,它可以控制多级的goroutine。 context翻译成...
Go语言探索 – 12(结局) 上一篇文章 文章主要学习了Go语言中的接口、反射以及错误和异常处理。本篇文章主要学习Go语言的协程,当然也是GO语言基础的最后一篇。 goroutine: goroutine是Go并行设计的核心,也是这门语言的精髓体现...