翻译:Defer,Panic和Recover

539 阅读3分钟

Go 拥有常见的控制流程机制:ifforswitchgoto。它还使用 go 语句在特定的 goroutine 中运行代码,这里我想讨论一些不太常见的:deferpanicrecover

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
}

这个函数能正常工作,但是有一个 bug。如果对 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 语句使我们可以在打开每个文件直接立即关闭它,能够保证不管函数里有多少 return 语句,文件都会被关闭

defer 语句的行为是直接和可预测的。有三条简单的规则:

  1. defer 语句求值时,将计算 defer 函数的参数 在这个例子中,当 Println 调用被延迟的时候计算表达式 i ,延迟调用将会再函数返回之后打印 0。

    func a() {
        i := 0
        defer fmt.Println(i)
        i++
        return
    }
    
  2. 在周围的函数 return 之后,defer 函数将会按照后进先出的顺序执行。 这个函数打印结果“3210”:

    func b() {
        for i := 0; i < 4; i++ {
            defer fmt.Print(i)
        }
    }
    
  3. defer 函数可以读取返回函数的指定返回值并复制给返回函数 下面的例子中,在周围的函数返回之后,defer 函数将增加返回值 i。因此,这个函数将会返回 2:

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

这便于修改函数的错误返回值;接下来我们将看到一个例子。

Panic 是一个内置函数,可以停止常规的控制流并开始 panicking,当函数 F 调用 panic,F 的执行就会停止,F 中的任何 defer 函数将会正常执行,然后 F 返回给它的调用者。对于调用者,F 的行为就像 panic 的调用。该进程将继续执行堆栈,直到返回当前 goroutine 的所有函数为止,此时程序崩溃。Panic 可以通过直接调用panic来启动。也可能是由运行时错误引起的,比如数组越界访问。

Recover 是一个内置函数,可以重新获得 panicking 的 goroutine。Recover 只有在 defer 函数内部才会生效。在正常执行期间,recover 的调用将返回 nil,没有其他的效果。如果当前的 goroutine 是 panicking 状态,调用 recover 将捕获给 panic 的值并恢复正常执行。

这有一个示例程序来演示 panicdefer 的机制:

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 获取整数 i,如果 i >3,将会引发 panic,否则它使用参数 i+1 调用自身。函数 f 延迟调用 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 package 代码。它使用一组递归函数对接口进行编码。如果遍历该值的时候发生错误,将会调用 panic 将堆栈退回到顶层函数调用,该函数将从 panic 中恢复适当的错误值(请参阅encode.go中encodeState类型的“error”和“marshal”方法)

Go 标准库的约定是,记使程序包内部使用了 panic,外部 API 仍然会显式地展示错误返回值

defer 的另外一种用法包括释放互斥锁

mu.Lock()
defer mu.Unlock()

打印页尾

printHeader()
defer printFooter()

还有其他没有给出的更多用法

总而言之,defer 语句(跟着或不跟着 panic、recover)为控制流提供了一种不寻常的、强大的机制。它可以用来建模由其他编程语言中的特殊结构实现的很多特性。尝试一下吧。

原文链接