β

Defer, Panic 和 Recover

JudyMaxiee's Blog 40 阅读

Defer, Panic 和 Recover 这三个概念是 Golang 中独有的, 也是最基础的, 因此有必要将他们弄懂.

Golang Blog 上有一篇 Defer, Panic, and Recover , 是很好的学习材料.

原文作者是 Andrew Gerrand, 本文是对这篇文章的笔记.

defer

defer 声明将一个函数调用添加到一个列表中. 这个列表储存了函数调用, 它们将在包含 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 失败了, 函数会直接退出, 而文件没有正常关闭.

这个问题是可以修正的, 例如可以对两个错误判断的 if 里面都加上文件关闭操作.

但是并不是所有的 Bug 都这么显而易见, 这就是 defer 引入语法索要解决的问题.

通过使用 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.被 deferred 的函数的参数在 defer 语句被求值时进行求值:

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

在本例中, defer fmt.Println(i) 中 i 的值取的是 1.

2.被 deferred 的函数的执行顺序按照后入先出的顺序:

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}

在上例中, 会先输出 3, 再 2, 再 1, 0.

3.被 deferred 的函数可以读写函数的有名字的返回值:

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

在上例中, 这个函数的返回值不是 1, 而是 2.

Panic

这是一个内置的函数, 它的作用是终止正常的控制流并且开始 panicking.

当一个函数 F 调用 panic, F 的执行终止, 任何被 deferred 的函数被正常执行.

The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes.

Panic 可以直接被 panic 方法触发, 也可以被运行时错误触发.

Recover

recover 是一个内置函数, 用于恢复一个 panicking goroutine 的控制.

recover 只在被 deferred 的函数中使用. 在普通执行期间, 调用 recover 将返回 nil, 并且没有别的效果.

如果当前 goroutine 是 panicking 的, 调用 recover 将捕获传入 panic 的值, 并恢复正常执行.

Panic Recover 实例

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, 否则会递归调用自己.

函数 f defer 了一个函数, 在其中调用 recover 并打印被 recover 的值.

现在问题来了, 这个函数会输出什么?

我第一个问题是 g 中的 defer, 是每次递归就执行一次 defer, 还是等全部递归完了再一次性按照后入先出调用呢? 答案是后者.

g(4) 触发了 panic 之后, f 的 defer 把它救起来了, 并成功收到了传入 panic 的值.

recover 之后, 程序就退出了, 因此 Returned normally from g. 这句并没有输出出来.

这个程序的输出如下:

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.

Panic Recover 实例二

另一个 panic 和 recover 的例子是标准库里的 json 包.

在解析 JSON 中如果出错, 解析器就会调用 panic 来释放栈顶的函数调用 (the parser calls panic to unwind the stack to the top-level function call), 从 panic 中 recover 并返回一个合适的错误值.

作者:JudyMaxiee's Blog
Judy和Maxiee的生活、学习点滴记录~
原文地址:Defer, Panic 和 Recover, 感谢原作者分享。

发表评论