延迟语句是什么?

申请资源,例如打开/关闭连接、加/释放锁、打开/关闭文件,在用完之后需要释放掉,否则会造成内存泄露。Go 在语言层面提供 defer 关键字,在申请资源语句的下一行,直接使用 defer 语句注册一个函数,执行释放资源的操作。可以达到这样的效果,是因为 defer 是一种注册延迟调用机制。
举一个不使用 defer 的反面例子,如下。

r.mu.Lock()
rand.Intn(args)
r.mu.Unlock

该例子只有三行代码,看起来不使用 defer 也没什么问题。但其实,中间这行代码是可能会发生 panic 的,又或者在中间有人添加了更多的逻辑,导致中间部分不可控,那这就可能造成死锁。简单的代码,用 defer 也是有必要的,因为需求总是可能变化,导致代码会被修改。

延迟语句的执行顺序

先进后出。最先被定义的 defer 语句最后执行,原因是后面定义的函数可能会依赖前面的资源。

延迟语句的外部引用

在定义 defer 语句后的函数时,外部变量的引用方式有如下两种。

  • 函数参数。
  • 闭包函数。
    使用前者的方式,定义 defer 时就会把值传递给 defer,并被 cache (也就是被复制了一份,为什么被复制一份?因为,加入传入的是一个引用,可能会导致在执行过程中,该变量被修改,与定义时不一致)。
    使用后者的方式,在真正执行 defer 后的函数时,会根据上下文确定当前参数的值。通过下面示例加强外部引用的理解。
// 闭包引用
func main() {
    var tmpArray [3]int

    for i := range tmpArray {
        defer func(){
            fmt.Println(i)
        }()
    }
}

执行结果如下:

2
2
2

image.png
执行结果如下:

3
3
3
0

记住,先进后出,首先执行的是 22 行的 defer 语句,它是一个闭包函数,引用外部函数的 n,最终返回 3;接下来是 19 行,与 22 行 defer 同理,闭包函数,根据上下文得到 n 为 3‘第 18 行,虽然它是函数参数的形式,但是它是引用,因此 n 为 3;最后是 17 行,它是函数参数的方式,同时没有引用,在定义时,n 被没有被 25 行赋值,所以 n 为 0。