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错误。
总结
对每一个知识点,都需要有准确的了解,尤其在本有机会去了解的时候,否则极易写出问题。
资料
-
golang中defer的使用规则
-
Go专家编程
最后
大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)
我的个人博客为:shidawuhen.github.io/
往期文章回顾:
转载自:https://juejin.cn/post/7068854815589138439