Go defer的一些神奇规则,你了解吗?

defer有一些规则,如果不了解,代码实现的最终结果会与预期不一致。对于这些规则,你了解吗?

测试题

这是关于defer使用的代码,可以先考虑一下返回值。

package main

import (
	"fmt"
)

/**
 * @Author: Jason Pang
 * @Description: 快照
 */
func deferFuncParameter1() {
	var aInt = 1
	defer fmt.Println(aInt)
	aInt = 2
	return
}

/**
 * @Author: Jason Pang
 * @Description: 快照
 */
func deferFuncParameter2() {
	var aInt = 1
	defer func(t int) {
		fmt.Println(t)
	}(aInt)
	aInt = 2
	return
}

/**
 * @Author: Jason Pang
 * @Description: 动态
 */
func deferFuncParameter3() {
	var aInt = 1
	defer func() {
		fmt.Println(aInt)
	}()
	aInt = 2
	return
}

/**
 * @Author: Jason Pang
 * @Description: 影响返回值
 * @return ret
 */
func deferFuncReturn1() (ret int) {
	ret = 10
	defer func() {
		ret++
		fmt.Println("-----", ret)
	}()
	return 2
}

/**
 * @Author: Jason Pang
 * @Description: 不影响返回值
 * @return ret
 */
func deferFuncReturn2() (ret int) {
	ret = 10
	defer func(ret int) {
		ret++
		fmt.Println("-----", ret)
	}(ret)
	return 2
}

/**
 * @Author: Jason Pang
 * @Description: defer顺序
 */
func deferFuncSeq1() {
	var aInt = 1
	defer fmt.Println(aInt)
	aInt = 2
	defer fmt.Println(aInt)
	return
}

func main() {
	fmt.Println("快照")
	deferFuncParameter1()
	deferFuncParameter2()
	deferFuncParameter3()
	fmt.Println("返回值")
	fmt.Println(deferFuncReturn1())
	fmt.Println(deferFuncReturn2())
	fmt.Println("执行顺序")
	deferFuncSeq1()
}

复制代码

正确输出为:

➜ myproject go run main.go

快照

1

1

2

返回值

----- 3

3

----- 11

2

执行顺序

2

1

分析

defer有几条重要规则,上面的结果都能从这些规则中找到答案。

规则一 当defer被声明时,其参数就会被实时解析

当defer被声明的时候,如果直接使用了参数,此时的参数就会使用快照值,在整个生命周期内不会变化。如deferFuncParameter1、deferFuncParameter2,虽然aInt在defer声明后被变更,但defer里的值不会再变了。

func deferFuncParameter1() {
	var aInt = 1
	defer fmt.Println(aInt)
	aInt = 2
	return
}

func deferFuncParameter2() {
	var aInt = 1
	defer func(t int) {
		fmt.Println(t)
	}(aInt)
	aInt = 2
	return
}

复制代码

与之相反的是deferFuncParameter3,随aInt的变化而变化。

func deferFuncParameter3() {
	var aInt = 1
	defer func() {
		fmt.Println(aInt)
	}()
	aInt = 2
	return
}

复制代码

规则二 defer可能操作主函数的具名返回值

defer有可能更改函数的返回值,这是最容易导致错误的地方。

关键字_return_不是一个原子操作,实际上_return_只代理汇编指令_ret_,即将跳转程序执行。比如语句 return i ,实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的。return i的执行过程如下所示:

result = i 
执行defer
return

复制代码

所以基于这个规则,对于deferFuncReturn1,

func deferFuncReturn1() (ret int) {
	ret = 10
	defer func() {
		ret++
		fmt.Println("-----", ret)
	}()
	return 2
}

复制代码

执行过程为:

ret = 2
ret++
fmt.Println("-----", ret)
return

复制代码

所以最终ret的值为3。

对于deferFuncReturn2,因为defer声明的时候直接使用了参数,所以使用的是快照,不会影响ret的返回值。

规则三 延迟函数执行按后进先出顺序执行,即先出现的 defer最后执行

这个规则大家都很熟悉,defer按照栈的顺序执行。

坑实例

举一个错误使用defer的实例。在go中使用事务时,有一种推荐写法:将Rollback放到defer中,通过判断函数是否有报错或者panic,来判断是否要回滚。

func  Update() (resp *baseinfo.Resp, err error) {
	//开启事务
	panicked := true
	tx, err := db.TXBegin()
	if err != nil {
		return resp, nil
	}
	defer func() {
		if panicked || err != nil {
			tx.Rollback()
		}
	}()

	//更新
	err = h.update(shopId, tx)
	if err != nil {//失败返回
		return resp, nil
	}

	panicked = false
	err = tx.Commit().Error
	if err != nil { //失败返回
		return resp, nil
	}
	return
}

复制代码

判断回滚的err正是函数的具名返回值,在有报错的情况下,返回值被赋值为nil,这意味如果有失败,Rollback也不会被执行。

之所以不将err直接返回,而是使用nil,是因为框架设计的问题,业务错误通过resp返回,如果直接返回err,框架会认为是RPC错误。

总结

对每一个知识点,都需要有准确的了解,尤其在本有机会去了解的时候,否则极易写出问题。

资料

  1. golang中defer的使用规则

  2. Go专家编程

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:shidawuhen.github.io/

往期文章回顾:

  1. 设计模式

  2. 招聘

  3. 思考

  4. 存储

  5. 算法系列

  6. 读书笔记

  7. 小工具

  8. 架构

  9. 网络

  10. Go语言

Supongo que te gusta

Origin juejin.im/post/7068854815589138439
Recomendado
Clasificación