清水泥沙

golang基础(37.defer 语句及使用示例)

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

func ReadFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer f.Close()

    var n int64 = bytes.MinRead

    if fi, err := f.Stat(); err == nil {
        if size := fi.Size() + bytes.MinRead; size > n {
            n = size
        }
    }
    return readAll(f, n)
}

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
", i, j, k)
}

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