Golang 中的 defer 关键字

0x00 defer 是啥

用一段简单的代码演示

package main

import (
	"fmt"
)

func main() {
    defer fmt.Println("this defer fmt!!")
    fmt.Println("this is normal fmt!!")
}

以上代码的输出如下:

Golang 中的 defer 关键字

defer 的作用就是注册一个方法的调用,该方法将在程序返回后进行输出。顺带,defer 的执行队列是一个链表,当使用一次 defer ,就会在链表的头结点插入一个执行结点,然后函数返回执行 defer 注册的操作时会依次取头结点执行,所以最后呈现的效果就是 “先入后出” 的执行顺序。

0x01 实诚的建议

defer 很好用,特别是用来关闭一切需要显式关闭的对象时,比如 file 的读写对象,golang 里协程的计数器 sync.WaitGroup ,在创建了一个类似的对象后为其注册一个 defer *.close() 调用绝对是一个很明智的选择。

除此之外不建议用 defer,如果用 defer 来计算函数返回某些数值的话,往往会得到预期之外的结果。举个栗子:

func defer_test1(){
    a, b := 1, 2
    defer add(a, b)
    a++
    b++
}

func add (a,b int){
    fmt.Println(a + b)
}

Golang 中的 defer 关键字

上述代码将输出 3 而不是使用 defer 设想的 5,出现这个问题的原因是因为 golang 中 defer 底层仍然采用的是值传递,当我们使用 defer 关键字时,它会向 defer 的执行队列里面拷贝引用的外层参数,比如上面这串代码中的 add 方法就是外层参数,那么内层参数 golang 又会怎么处理呢?上面说到 defer 的函数调用也是值传递的,它会将内层参数直接计算出来然后传递给外层参数,也就是 a = 1, b = 2 ,所以计算出来是 3

这个时候就肯定有人想,既然是因为值传递引起的这个问题,那么我强制使用指针传递引用会发生什么呢?结果显而易见,引用传递的值将得到预期的 5

func defer_test1(){
    a, b := 1, 2
    defer add(&a, &b)
    a++
    b++
}

func add (a,b *int){
    fmt.Println(*a + *b)
}

Golang 中的 defer 关键字

但是,你以为这就结束了嘛 -~-,当然没有!如果吧代码改成下面这样呢

func defer_test1(){
    a, b := 1, 2
    defer fmt.Println(add(&a, &b))
    a++
    b++
}

func add (a,b *int) int{
    fmt.Println(*a + *b)
    return *a + *b
}

func add2(a, b int) int{
    return a + b
}

会输出 3 还是 5 ?答案是 3

Golang 中的 defer 关键字

对于这个输出的理解,我是这样子理解的:对于 defer 来说外层参数为 fmt.Println 内层参数为 add 这里会拷贝 add 的值,也就是计算后的返回值 3。再写个代码检验一下。

func defer_test1(){
	a, b := 1, 2
	defer add2(add(&a, &b), add(&a, &b))
	a++
	b++
}

func add (a,b *int) int{
    fmt.Println(*a + *b)
    return *a + *b
}

func add2(a, b int) int{
    return a + b
}

预期结果是 6 ,实际输出跟预期一致

Golang 中的 defer 关键字

0x02 不需要建议

那如果非要在结束时计算呢?我们可以使用匿名函数的方式来达到效果。

func defer_test1(){
	a, b := 1, 2
	defer func (){
		fmt.Println(a + b)
	}()
	a++
	b++
}

输出:

Golang 中的 defer 关键字

如上,就能够达到我们的目的。

鸣谢

Go 语言设计与实现》第 5.3 节

上一篇:关于 async 和 defer 的作用及不同之处(浏览器解析过程)


下一篇:【NodeMCU】01 使用WIFI或IR点亮3色LED灯同时使用U8G2驱动0.96寸OLED