Go 中的 defer 详解

基础

defer 是 Go 语言提供的一种用于注册延迟调用的机制:让函数或语句可以在当前函数执行完毕后(包括通过 return 正常结束或者 panic 导致的异常结束)执行。

  1. 每次 defer 语句执行的时候,会把函数“压栈”,函数参数会被拷贝下来。
  2. 当外层函数退出时,defer 函数按照定义的逆序执行(先进后出)。
  3. 如果 defer 执行的函数为 nil, 那么会在最终调用函数的产生 panic。

不能在defer里面返回值

defer 函数对外部变量的引用

在 defer 函数定义时,对外部变量的引用是有两种方式的,分别是作为函数参数和作为闭包引用。

  • 作为函数参数,则在 defer 定义时就把值传递给 defer,并被 cache 起来。
  • 作为闭包引用的话,则会在defer 函数真正调用时根据整个上下文确定当前的值。

defer 后面的语句在执行的时候,函数调用的参数会被保存起来,也就是复制了一份。真正执行的时候,实际上用到的是这个复制的变量,因此如果此变量是一个“值”,那么就和定义的时候是一致的。如果此变量是一个“引用”,那么就可能和定义的时候不一致。

defer 后面跟的是闭包,必然是引用类型的变量。

执行的时机

什么情况下会调用 defer 延迟过的函数呢?

  1. 当函数执行了 return 语句后
  2. 当函数处于 panicing 状态,也就是程序中 panic 回溯上来到当前函数范围内时

对于第一条,首先要了解到 return xxx 不是原子命令,会被拆分成如下三行。

返回值 = xxx
调用 defer 函数
空的 return

defer 会被插入到赋值和返回之间执行。 来看看如何拆解。

func f() (r int) {
     t := 5
     defer func() {
       t = t + 5
     }()
     return t
}
// 拆解后
func f() (r int) {
     t := 5
     r = t // 1. 赋值指令
     func() {
         t = t + 5 // 2. defer 被插入到赋值与返回之间执行,这个例子中返回值 r 没被修改过
     }
     return // 3. 空的 return 指令
}
func f() (r int) {
    defer func(r int) {
          r = r + 5
    }(r)
    return 1
}
// 拆解后
func f() (r int) {
     r = 1 // 1. 赋值
     func(r int) {
          r = r + 5 // 2. 这里改的 r 是之前传值传进去的r,不会改变要返回的那个r值
     }(r)
     return // 3. 空的 return
}

defer 和 闭包

其实面试时候,考察是否掌握了 defer 无非是看 defer 的函数是否是闭包。以及 return 语句的拆解。

func f1() {
	var err error
	defer fmt.Println(err)
	err = errors.New("defer error")
	return
}
func f2() {
	var err error
	defer func() {
			fmt.Println(err)
	}()
	err = errors.New("defer error")
	return
}
func f3() {
	var err error
	defer func(err error) {
			fmt.Println(err)
	}(err)
	err = errors.New("defer error")
	return
}
func main() {
	f1()
	f2()
	f3()
}

这段函数输出值如下:

<nil>
defer error
<nil>

f3() 很多人会搞错,以为也会输出 defer error。但只要学会区分什么是闭包,就不会发生拿不准返回值的问题。

使用

资源的释放

一些成对操作,需要回收资源的场景:打开连接/关闭连接;加锁/释放锁;打开文件/关闭文件等。

resA, err:= getA()
if err != nil {
    return
}
// 这里是经典范式:先判断对错,再释放资源。
defer ReleaseA()

resB,err := getB(resA)
if err != nile {
    return
}
defer ReleaseB()

异常处理

panic 以及 recover 组合起来模拟 try...catch 功能。

func protect(g func()) {
    defer func() {
        log.Println("done")  // Println executes normally even if there is a panic
        if x := recover(); x != nil {
            log.Printf("run time panic: %v", x)
        }
    }()
    log.Println("start")
    g()
}

g() 中通过 panic 抛出错误时,会在 defer 中用 recover 进行捕获。也就是在子函数中的 panic 触发了其处于 panicing 状态,从而当 panic 回溯到当前函数时调用本函数的 defer 修饰的函数。

Go 官方不建议这么做。

考题

defer 的面试题一般都会考察 匿名返回值 和 命名返回值 之间的区别。

匿名返回值

func main() {
	fmt.Println("a return:", a()) // 打印结果为 a return: 0
}

func a() int {
	var i int
	defer func() {
		i++
		fmt.Println("a defer2:", i) // 打印结果为 a defer2: 2
	}()
	defer func() {
		i++
		fmt.Println("a defer1:", i) // 打印结果为 a defer1: 1
	}()
	return i
}

输出结果:

a defer1: 1
a defer2: 2
a return: 0

匿名返回值是在 return 之前被声明(鉴于类型原因,类型零值为0),defer 无法访问匿名的返回值,因此返回值是 0,而 defer 还是操作之前定义好的变量 i。

命名返回值

func main() {
	fmt.Println("a return:", a()) // 打印结果为 b return: 2
}

func a() (i int) {
	defer func() {
		i++
		fmt.Println("a defer2:", i) // 打印结果为 b defer2: 2
	}()
	defer func() {
		i++
		fmt.Println("a defer1:", i) // 打印结果为 b defer1: 1
	}()
	return i // 或者直接 return 效果相同
}

输出结果:

a defer1: 1
a defer2: 2
a return: 2

命名返回值是在函数声明的同时被声明。因此 defer 可以访问命名返回值。return 返回后的值其实是 defer 修改后的值。

作用域

在什么环境下就不会执行?

  1. 当任意一条(主)协程发生 panic 时,会执行当前协程中 panic 之前已声明的 defer;
  2. 主动调用 os.Exit(int) 退出进程时,defer 将不再被执行
  3. 在发生 panic 的(主)协程中,如果没有一个 defer 调用 recover()进行恢复,则会在执行完之前已声明的 defer 后,引发整个进程崩溃;
  4. defer 只对当前(主)协程有效

相关链接

如果觉得我的文章对您有用,请在支付宝公益平台找个项目捐点钱。 @Victor May 14, 2019

奉献爱心