Go语言defer的一些神奇规则示例详解
程序员麻辣烫 人气:0测试题
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错误。
加载全部内容