CodeKiller
Go 语言 Defer, Panic 和 Recover

认识 defer

defer 是 go 语言的一个关键字,在一个函数之前使用 defer,这个函数会在当前函数返回之后被调用。Defer 经常被用来做资源释放、日志打印、异常捕获。

多个 defer 函数的执行顺序 (LIFO)

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

该函数的打印结果是:3210,所以结论是:

多个 defer 函数按调用顺序倒序执行,类似于压栈出栈操作,但是 defer 的底层并不是一个栈,而是一个链表。

defer 函数参数预计算

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

上面这个例子会打印0,而不是1,所以结论是:

一个函数被 defer 的时候,它的参数会在当时立刻被计算出来,然后进行参数拷贝

defer 函数内部读写外部变量

func c() (i int) {
    defer func() { 
        i++  // i 对于这个匿名函数来说是外部变量
    }()
    return 1
}
fmt.Println(c())

上面这个例子会打印2,所以结论是:

一个函数被 defer 的时候,它内部使用的外部变量,会在最终执行的时候计算

认识 panic

首先,panic 是 go 语言的一个内置函数而不是一个关键字,panic 可以停止当前 goroutinue 的正常运行,比如,一个 goroutinue 中有多层函数嵌套,内层函数调用 panic 的时候,内层函数的正常执行被立即停止,该停止行为会层层传递给所有的外层函数(如果该 panic 没有被 recover 的话), 并会导致整个程序 crash。

panic 和 defer

defer 那一节讲到被 defer 的函数会在调用函数 return 之后被执行,那么如果调用函数 panic 的话,defer 的函数还会被执行嘛?

直接上例子:

func f() {
    defer fmt.Println("111")
    panic("Panicking")
    defer fmt.Println("222") // 该 defer 语句本身不会被执行
}

上面的例子打印结果如下:

111
panic: Panicking

goroutine 1 [running]:
main.f()
        /xxx/gopath/src/xxx.go:30:25 +0x95
main.main()
        /xxx/gopath/src/xxx.go:30 +0x20

Process finished with exit code 2

我们看到 111 被打印了出来,那么就说明:

panic 之后,被 defer 的函数仍然会被调用,并且是在 panic 打印堆栈之前。

而 222 没有被打印出来,是因为还没来得及 defer,该函数就被 panic 终止了。

认识 recover

panic 一样,recover 也是 go 语言的一个内置函数,上节说 panic 会导致整个程序 crash,如果是线上程序发生 panic 导致整个程序 crash,那就太危险了,那能不能即使发生 panic 了,也不要让整个程序 crash 呢?答案就是使用 recover 来”捕获“ panic

正常使用 recover 的方式:

package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    defer func() {  // 注意,recover 一定要放到 defer 函数里面
        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)
}

上面的程序输出如下:

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.

跨协程 recover 失效

前几天面试腾讯的时候,面试官问到这样一个问题:

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()

    go func() {
        panic("panic....")
    }()
}

这段代码中的 recover 可以捕获下面的 panic 嘛?

这段代码看似好像是函数嵌套调用发生的 panic,但是这个 panic 是发生在新起的另外一个 Goroutine 中的,所以答案是不能被 recover。

因为:

多个 Goroutine 之间没有太多的关联,一个 Goroutine 不会管也不应该管另外一个 Goroutine 发生的事情。

参考


Last modified on 2020-04-16