Go 博客

延迟、恐慌和恢复

Andrew Gerrand
2010 年 8 月 4 日

Go 拥有用于控制流的常用机制:if、for、switch、goto。它还具有 go 语句,用于在单独的 goroutine 中运行代码。在这里,我想讨论一些不太常用的机制:defer、panic 和 recover。

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 语句允许我们在打开每个文件后立即考虑关闭它,保证无论函数中有多少个 return 语句,文件都将被关闭。

defer 语句的行为简单易预测。有三个简单的规则

  1. 延迟函数的参数在评估 defer 语句时进行评估。

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

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}
  1. 延迟函数调用在周围函数返回后以后进先出 (LIFO) 的顺序执行。

此函数打印“3210”

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}
  1. 延迟函数可以读取和赋值给返回函数的命名返回值。

在此示例中,延迟函数在周围函数返回之后递增返回值 i。因此,此函数返回 2

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

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

Panic 是一个内置函数,它会停止正常的控制流并开始恐慌。当函数 F 调用 panic 时,F 的执行停止,F 中的任何延迟函数都正常执行,然后 F 返回到其调用者。对于调用者来说,F 然后表现得像对 panic 的调用一样。该过程一直持续到当前 goroutine 中的所有函数都返回,此时程序崩溃。可以通过直接调用 panic 来启动恐慌。它们也可能由运行时错误(例如数组越界访问)引起。

Recover 是一个内置函数,它可以重新获得恐慌 goroutine 的控制权。Recover 仅在延迟函数内部有用。在正常执行期间,对 recover 的调用将返回 nil 并且不会产生其他影响。如果当前 goroutine 正在恐慌,则对 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 则恐慌,否则它会使用参数 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.

如果我们从 f 中删除延迟函数,则恐慌不会被恢复并到达 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 包。它使用一组递归函数对接口进行编码。如果在遍历值时发生错误,则调用 panic 以将堆栈展开到顶级函数调用,该函数从 panic 中恢复并返回适当的错误值(请参阅encode.go中 encodeState 类型的“error”和“marshal”方法)。

Go 库中的约定是,即使包在内部使用 panic,其外部 API 仍然提供明确的错误返回值。

defer 的其他用途(超出前面给出的 file.Close 示例)包括释放互斥锁

mu.Lock()
defer mu.Unlock()

打印页脚

printHeader()
defer printFooter()

等等。

总之,defer 语句(无论是否使用 panic 和 recover)提供了一种不寻常且强大的控制流机制。它可用于模拟其他编程语言中由特殊用途结构实现的一些功能。试试看。

下一篇文章:Go 荣获 2010 年 Bossie 奖
上一篇文章:通过通信共享内存
博客索引