golang中defer的理解与使用

2022-10-28 11:38:45

defer,即延迟调用,是Go语言的一大特色。defer代码块会在函数调用链表中增加一个函数调用,在函数正常返回,即return返回之后,增加一个函数调用。因此,defer常用来回收资源,哪怕程序执行有错误,依然能够保证回收资源等操作能够执行。

它的使用有三条规则:

  1. 当defer被声明时,其参数就会被实时解析
  2. defer执行顺序,为先进后出
  3. defer可以读取有名返回值

那么这三条规则具体是什么含义呢,可以通过具体的例子来看:

func add(){
	i := 10
	defer fmt.Println(i)
	i++
	return
}

最终程序会输出10,这是因为虽然我们在defer后面跟了一个带变量的函数: fmt.Println(i). 但这个变量i在defer被声明的时候,就已经确定值了。即参数会被实时解析。因此这段代码等同于:

func add(){
	i := 10
	defer fmt.Println(10)
	i++
	return
}

继而可以看下面的例子:

func f1() (result int) {
	defer func() {
		result++
	}()
	return 0
}

func f2() (r int) {
	t := 5
	defer func() {
		t = t+5
	}()
	return t
}

func f3() (t int) {
	t = 5
	defer func() {
		t = t+5
	}()
	return t
}
func f4() (r int) {
	defer func(r int) {
		r = r + 5
	}(r)
	return 1
}

这4个函数依次执行结果是1,5,10,1。函数返回的过程是先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。defer表达式可能会在设置函数返回值之后,在返回到调用函数之前,修改返回值,使最终的函数返回值与你想象的不一致。

因此,我们可以将return xxx改成 返回值=xxx 调用defer函数 return空 的形式,那上面的例子就可以改成:

func f11() (result int) {
	result = 0            //先给返回值赋值
	func(){               //再执行defer 函数
		result++
	}()
	return                //最后返回
}

func f22() (r int) {
	t := 5
	r = t     //赋值指令
	func() {  //defer 函数被插入到赋值与返回之间执行,这个例子中返回值r没有被修改
		t = t+5
	}
	return   //返回
}

func f33() (t int) {
	t = 5        //赋值指令
	func(){
		t = t+5  //然后执行defer函数,t值被修改
	}
	return
}
func f44() (r int) {
	r = 1          //给返回值赋值
	func(r int){   //这里的r传值进去的,是原来r的copy,不会改变要返回的那个r值
		r = r+5
	}(r)
	return
}

规则2就比较简单,最直观的方式是把它放入for循环中,观察结果就能得知:

func f() {
	for i := 1; i < 5; i++ {
		defer fmt.Print(i)
	}
}

运行结果为4321,先进后出的顺序~

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

规则3:

func f() (i int) {
	defer func() { i++ }()
	return 1
}

这里,函数定义了返回值i,最终return 1将i赋值为1,然后执行defer,进行自增操作,最终结果为2。虽然defer是在return之后执行,但是它的作用域仍然在函数之内。

可以总结一下,如果函数的返回值是无名的,那个golang会在执行return的时候会执行一个类似创建一个临时变量作为保存return值的动作,而有名返回值的函数,由于返回值在函数定义的时候已经将该变量进行定义,在执行return的时候会先执行返回值保存操作,而后续的defer函数会改变这个返回值(虽然defer是在return之后执行的,但是由于使用的函数定义的变量,所以执行defer操作后对该变量的修改会影响到return的值)
下面再综合给出4个易错的例子,左下与左上和上面讲述的例子一致,右上先定义了局部变量i,但是返回值是无名的int,因此i++没啥作用,最后return i相当于return 10,即创建一个临时变量保存return的值,右下与右上进行对比,右下是指针操作,操作的是该临时变量的地址值,因此可以改变return的内容。

最后说一句,延迟调用的确是个好东西,但是他不是一个简单的操作,涉及到了对象的分配,缓存以及多次函数调用,因此在对性能要求非常高的场合,最好少用defer。

文中例子参考:https://blog.csdn.net/huang_yong_peng/article/details/82950743

  • 作者:yyyyyhu
  • 原文链接:https://blog.csdn.net/huyang0304/article/details/103466435
    更新时间:2022-10-28 11:38:45