Go 博客

Defer、Panic 和 Recover

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
}

这可以工作,但有一个 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 语句被评估时评估。

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

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

此函数打印“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 是一个内置函数,它停止正常的控制流并开始panic。当函数 F 调用 panic 时,F 的执行停止,F 中的任何延迟函数正常执行,然后 F 返回到其调用者。对于调用者,F 表现得像一个对 panic 的调用。这个过程沿着堆栈继续,直到当前 goroutine 中的所有函数都已返回,此时程序崩溃。Panic 可以通过直接调用 panic 来启动。它们也可以由运行时错误引起,例如数组越界访问。

Recover 是一个内置函数,它重新获得对 panicking goroutine 的控制。Recover 仅在延迟函数内部有用。在正常执行期间,对 recover 的调用将返回 nil 并且没有其他效果。如果当前 goroutine 正在 panicking,对 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 并打印恢复值(如果它不为 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 中移除,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 包。它使用一组递归函数编码接口。如果在遍历值时发生错误,则调用 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 奖
上一篇文章:通过通信共享内存
博客索引