在go中当我们使用完某个资源需要将其释放,比如(网络连接,文件句柄),或者在代码运行过程中抛出错误时执行一段兜底逻辑,要怎么做呢?通过 defer 关键字声明兜底执行或者释放资源的语句可以轻松解决这个问题。比如我们看 Go 内置的 io/ioutil 包中提供的读取文件方法 ReadFile 实现源码,其中就有 defer 语句的使用:

    1. func ReadFile(filename string) ([]byte, error) {
    2. f, err := os.Open(filename)
    3. if err != nil {
    4. return nil, err
    5. }
    6. defer f.Close()
    7. var n int64 = bytes.MinRead
    8. if fi, err := f.Stat(); err == nil {
    9. if size := fi.Size() + bytes.MinRead; size > n {
    10. n = size
    11. }
    12. }
    13. return readAll(f, n)
    14. }

    defer 修饰的 f.Close() 方法会在函数执行完成后或读取文件过程中抛出错误时执行,以确保已经打开的文件资源被关闭,从而避免内存泄露。如果一条语句干不完清理的工作,也可以在 defer 后加一个匿名函数来执行对应的兜底逻辑:

    defer func() { 
        //  执行复杂的清理工作... 
    } ()
    

    另外,一个函数/方法中可以存在多个 defer 语句,defer 语句的调用顺序遵循先进后出的原则,即最后一个 defer 语句将最先被执行,相当于「栈」结构,如果在循环语句中包含了 defer 语句,则对应的 defer 语句执行顺序依然符合先进后出的规则。
    由于 defer 语句的执行时机和调用顺序,所以我们要尽量在函数/方法的前面定义它们,以免在后面执行时漏掉,尤其是运行时抛出错误会中断后面代码的执行,也就感知不到后面的 defer 语句。
    下面我们看一个简单的示例 defer.go:

    package main
    
    import "fmt"
    
    func printError()  {
        fmt.Println("兜底执行")
    }
    
    func main()  {
        defer printError()
        defer func() {
            fmt.Println("除数不能是0!")
        }()
    
        var i = 1
        var j = 1
        var k = i / j
    
        fmt.Printf("%d / %d = %d\n", i, j, k)
    }
    

    这段代码中,我们定义了两个 defer 语句,并且是在函数最顶部,以确保异常情况下也能执行。
    在函数正常执行的情况下,这两个 defer 语句会在最后一条打印语句执行完成后先执行第二条 defer 语句,再执行第一条 defer 语句。