小马别过河

V1

2022/09/27阅读:22主题:默认主题

go 概念[译]: Defer, Panic, and Recover

Go语言有常用的流控制机制:if、for、switch、goto。同时go关键字可以让代码跑在独立的协程上(goroutine)。本文介绍比较少见的几个:defer、panic 和 recover。

defer

defer关键字把一个函数调用追加到一个列表。等defer所在的函数 return 以后,这个列表内的函数,会依次被执行。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调用失败,函数会在没有关闭 src 句柄的情况下,直接return。这个bug很容易修复,只要在第二个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语句很直接且可预测。这里有3个简单的规则。

1. defer函数的参数在defer出现时,就已经确定了。

比如下面例子,参数iPrintln被加入 defer 列表的时候,就已经确定了。所以下面的例子会打印"0"。

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

2. defer函数调用顺序是后进先出(LIFO)。

所以下面的输出是"3210":

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

3. defer函数可以对返回值进行读取和赋值。

下面的例子,会先确定i = 1,然后执行 i++,最后return i。所以返回值是2。

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

Panic 和 Recover

Panic是一个内置函数,用于中断控制流并开始"恐慌"(panicking)。当函数F调用panic时:

  1. F函数停止执行
  2. defer 函数正常调用
  3. F函数返回给调用者

对于F的调用者,相当于调用了panic,直到所有进程的调用栈都返回了,此时程序会崩溃(crash)。Panics 可以通过调用 panic函数直接触发,也可以由运行时引发,比如访问数组越界等。

Recover是一个内置函数,用于重新取得一个已经 panic 的 goroutine 的控制权。Recover只能用在 defer 函数里面,在其他地方调用 recover,只会返回 nil,不会有其他效果。如果当前 goroutine 正在 panic,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的参数i如果大于3就会panic,否则就i+1递归调用自己。函数f在 defer 调用 recover 方法,并打印捕获到的 panic 的值。

程序输出如下:

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的 recover 移除掉,panic就会返回到调用栈的最顶部,接着程序中断。输出如下:

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]

有关 panic 和 recover 的真实示例,可以参阅 Go 标准库的 json 包。该包通过一系列递归函数 encode 一个 Interface。如果在遍历的过程中出现错误,panic 会被触发,再由顶层的调用点捕获,接着返回一个合理的 error(参阅 encodeState的errormarshal方法)。

Go 标准库的约定是,如果一个包内部使用 panic,其对外的 API 仍然会返回一个明确的 error。

其他的 defer 的使用场景除了上文的释放文件资源,还有释放锁:

mu.Lock()
defer mu.Unlock()

打印footer:

printHeader()
defer printFooter()

等等。

总之,defer 语法(不管有没有 panic 和 recover)提供了一种不寻常且强大的控制流的机制。

参考资料

[1] https://go.dev/blog/defer-panic-and-recover

分类:

后端

标签:

Golang

作者介绍

小马别过河
V1