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
}

这样做可以,但是有一个错误。如果调用 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. 延迟函数调用在外部函数返回后以 Last In First Out (后进先出) 的顺序执行。

这个函数会打印“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 可以通过直接调用 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 则发生 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 奖
上一篇文章:通过通信共享内存
博客索引