hello world

stay foolish, stay hungry

golang中的defer, panic和recover

Go提供了 defer, panicrecover 三个内置方法。其中 panic 会让程序崩溃,defer 可以在函数 return 之前执行操作, deferrecover 配合可以捕获 panic。

defer

defer 声明的语句可以在函数或方法返回(不管是正常返回或异常返回)之前调用,类似于 Java 里面的 finally,可以做一些清理的工作,比如关闭文件、 释放资源等操作。

程序 1 展示了 defer 的一般的用法,通过 defer 语句保证 srcdst 最终会被释放。

// 程序 1
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 语句有三个约定:

  1. defer 语句参数的值在 defer 语句声明时就已经确定了

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

    defer 语句并不是简单的延迟执行,程序 2 中的 a() 方法执行到 defer fmt.Println(i) 时,会将i的值 copy 一份和defer语句的声明一起入栈,在 return 之前,声明的 defer 语句出栈执行,所以程序 2 最终打印出i的值是0

  2. defer 语句的执行顺序是后进先出(LIFO)

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

    defer 语句的声明和执行可以看作是 defer 语句块的「入栈」和「出栈」操作,先声明的 defer 语句最后执行,所以程序 3 的输出是 3201

  3. defer 语句可以读取并修改外部函数命名的返回值(named return values)

    // 程序 4
    func c() (i int) {
        defer func() { i++ }()
        return i
    }
    

    defer 语句可以在 return 之前执行, 并且可以修改外部函数命名的返回值(named return values)。程序 4 在 return 之前会执行 defer 语句,所以程序 4 最终返回的是 1

    // 程序 5
     func c() int {
        var i int
        defer func() { i++ }()
        return i
    }
    

    但程序 5 最终输出的是 0

panic和recover

panic 内置函数可以让让当前 goroutine 崩溃,当函数 F 中调用了或者触发了 panic,F 会立即终止运行,然后执行 F 中的 defer 语句,然后 F 返回到调用者 G,G 也会立即终止运行,然后执行 G 中的 defer 语句,这样一层一层的向上返回,直到顶层的 goroutine(函数调用链的顶层 goroutine,不一定是 main goroutine),然后程序崩溃。

recover 内置方法可以再次控制 panic 的 goroutine。recover 方法只有在 defer 中才有效果。正常情况下,recover 会返回 nil,如果当前 goroutine 发生了 panic,recover 方法会捕获 panic 的值,并且再次获得程序的控制权。

// 程序 6
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)
}

程序 6 中 g 的逻辑是如果 i>3,则 panic,否则递归进行 i+1,f 在 defer 中调用了 recover,并打印了 recover 信息,程序的输出:

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中的 defer 声明,panic 不会 recover,一层一层的返回 panic,直到 goroutine 调用栈的顶端,然后程序崩溃。去掉 defer 语句之后的输出:

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]

recover 返回的是 panic 的值,以下三种情况,recover 的值是 nil

  • panic 的值是 nil
  • goroutine 没有 panic
  • recover 没有直接在 defer 中调用

前两种情况好理解,第三种情况,如果 recover 没有在 defer 中直接调用,那么 recover 就不能捕获 panic。下面三段代码,程序 7 中的 recover() 可以正常捕获 panic,而程序 8 和程序 9 则会 panic。

// 程序 7
package main

import "fmt"

func main() {
    a()
}

func a() {
    defer r()
    panic("a")

}

func r() {
    if r := recover(); r != nil {
        fmt.Println(r)
    }
}
// 程序 8
package main

import "fmt"

func main() {
    a()
}

func a() {
    defer func() {
        r()
    }()
    panic("a")

}

func r() {
    if r := recover(); r != nil {
        fmt.Println(r)
    }
}
// 程序 9
package main

import "fmt"

func main() {
    a()
}

func a() {
    r()
    panic("a")

}

func r() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println(r)
        }
    }()
}

总结

这篇文章介绍了 golang 内置的 defer、panic 和 recover 函数,我们了解到:

  • defer 声明时参数就确定了,不会随着方法内部代码的执行而变化。
  • defer 执行顺序是后进先出(LIFO)。
  • defer 可以读取并修改外部函数命名的返回值(named return values)。
  • recover 必须在 defer 中,并且是 defer 直接调用才能捕获 panic

参考

  1. https://golang.org/ref/spec#Handling_panics
  2. https://blog.golang.org/defer-panic-and-recover