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错误。

以上就是Go语言defer的一些神奇规则示例详解的详细内容,更多关于Go语言defer规则的资料请关注其它相关文章!

原文地址:https://juejin.cn/post/7068854815589138439