Defer, Panic 和 Recover

未匹配的标注

本文为官方 Go Blog 的中文翻译,详见 翻译说明

Andrew Gerrand
2010 年 8 月 4 日

Go 具有常见的控制流程机制: if, for, switch, goto. 还有可以使函数在独立协程中运行的 go 语句. 这里我们讨论下一些不那么常见的语句: 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声明时就会求值

在下面的示例中, 当延迟调用 Println 时将会对表达式 “i” 求值. 函数返回后, 延迟调用将显示 “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 是一个内置函数用来停止常规控制流程并开始 恐慌. 当函数 F 调用 panic 函数时, 后续执行会停止, 但函数内的任何延迟函数会正常执行, 随后返回 F 给调用者. 对调用者而言, F 表现为恐慌中. 该过程将继续执行堆栈, 直到返回当前协程中的所有函数为止, 此时程序彻底崩溃. 恐慌事件可以通过直接调用 painc 函数触发. 它们也可能是由运行时错误引起的, 例如函数越界访问数组.

Recover 是一个内置函数用来重新获取对失控协程的控制. 恢复仅在延迟函数内有效. 在正常执行期间, 调用 recover 函数将返回 nil 并且没有卵用. 如果当前协程处理恐慌状态中, 调用 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 defer一个函数,该函数调用recover并且打印这个recovered的值(如果它不是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中删除这个deferred函数,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

Go库中的约定是,即使程序包内部使用了panic,其外部API仍会显示显式的错误返回值。

defer的其他用途(不只是之前提供的file.Close示例)还包括释放互斥锁:

mu.Lock()
defer mu.Unlock()

打印页脚:

printHeader()
defer printFooter()

和更多。

总而言之,defer语句(带有或不带有panic和recover)提供了一种异常强大的控制流机制。它可以用来建模由其他编程语言中的专用结构实现的许多功能。试试看。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/go-blog/defer-p...

译文地址:https://learnku.com/docs/go-blog/defer-p...

上一篇 下一篇
Summer
贡献者:4
讨论数量: 0
发起讨论 只看当前版本


暂无话题~