Go Defer, Panic, Recover

翻译

https://blog.golang.org/defer-panic-and-recover

TLTR

  • defer的函数在压栈的时候也会保存参数的值,而不是在执行时取值。换句话说,就是defer函数的参数在defer语句出现时,就已经确定下来了。
  • 当外围函数执行完成之后,defer 函数是以后进先出的方式执行的
  • defer 函数可以对函数的返回值进行读写

正文

Go有着常见的控制流:if, for, switch, goto。还有go这样的关键字来开启协程。这里我想讨论不那么常见的控制流:defer、panic和recover。

defer 语句将函数调用推送到list中。保存着这些函数调用的列表将会在调用defer语句的函数返回时执行。Defer常用于简化clean-up函数的调用。

译者注:可能用栈会更合适一些,defer语句的调用顺序是后进先出的。原文为list

译者注:clean-up函数,如流的关闭等

举个例子,让我们来看一个打开文件A、创建文件B、并将A文件的内容拷贝到文件B的函数

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的时候出错,打开的文件A没有关闭。通过在第9行添加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语句的行为直接且可预测。有三条基本的规则

  1. defer的函数在压栈的时候也会保存参数的值,而不是在执行时取值。换句话说,就是defer函数的参数在defer语句出现时,就已经确定下来了。

在这个例子中,在defer PrintCall函数时,表达式i的值就被计算了。该语句会输出0,而不是1

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}
  1. 当外围函数执行完成之后,defer 函数是以后进先出的方式执行的
func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}
  1. defer 函数可以对函数的返回值进行读写

下面这个例子在包围的函数返回后,增加了i的值。这个函数会返回2

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

这样可以很方便地修改函数的错误返回值; 我们很快就会看到一个例子。

Panic 是一个用来停止原有的控制流的内置函数,然后开始panicking。 当函数F调用panic之后,函数F的执行停止了,F中的任何defer函数都会执行,然后F返回给F的调用者。接下来对于调用者来说,调用程序F就像调用了panic一样。这个过程一直递归向上,直到当前goroutine的所有程序都返回。Panic可以通过直接调用panic来启动。也可以由运行时错误引起,如越界数组访问等。

译者注:在defer函数中的panic也会执行

Recover 是一个可以重新控制panicking的goroutine的函数。Recover仅能在defer函数中调用。在正常的执行流程中,对recover的调用会返回nil并且不会造成任何影响。如果当前协程处于panicking的状态,recover的调用会捕获到这个值,并且继续正常的执行流

这是一个用来展示panicrecover机制的样例程序

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 defer调用recover 函数并打印恢复的值(如果它是非nil)。 在继续阅读之前,试着想象一下这个程序的输出可能是什么?

程序将会输出:

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.

如果我们把defer函数从f中移除,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]

panicrecover非常适合用来语法遍历的时候,需要回溯的场景。像go的标准库json包中也使用到了panicrecover。它使用一组递归函数对接口进行编码。 如果在遍历值时发生错误,则调用 panic 将堆栈unwind到顶级函数调用,该函数调用从 panic 中恢复并返回适当的错误值。

Go库中约定,即使包在内部使用 panic,其外部 API 仍会显示明确的错误返回值。

其他的defer例子:

defer中关闭锁:

mu.Lock()
defer mu.Unlock()

打印footer

printHeader()
defer printFooter()

等等

总而言之,defer为控制流提供了一种不寻常且强大的机制。 它可用于对由其他编程语言中的专用结构实现的许多功能进行建模。 现在就用起来吧!

译者注:就是说defer的功能十分强大。

你可能感兴趣的:(Go Defer, Panic, Recover)