golang-defer坑的本质
来源:互联网 发布:淘宝二手苹果7可信吗 编辑:程序博客网 时间:2024/06/05 22:53
本文节选自https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.4.html
作者的分析非常透彻,从问题本质分析,就不会对defer产生的副作用产生迷茫。
defer坑的本质是:本质原因是return xxx语句并不是一条原子指令,defer被插入到了赋值 与 ret之间,因此可能有机会改变最终的返回值。
defer使用时的坑
先来看看几个例子。例1:
func f() (result int) { defer func() { result++ }() return 0}
例2:
func f() (r int) { t := 5 defer func() { t = t + 5 }() return t}
例3:
func f() (r int) { defer func(r int) { r = r + 5 }(r) return 1}
请读者先不要运行代码,在心里跑一遍结果,然后去验证。
例1的正确答案不是0,例2的正确答案不是10,如果例3的正确答案不是6......
defer是在return之前执行的。这个在 官方文档中是明确说明了的。要使用defer时不踩坑,最重要的一点就是要明白,return xxx这一条语句并不是一条原子指令!
函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。
defer表达式可能会在设置函数返回值之后,在返回到调用函数之前,修改返回值,使最终的函数返回值与你想象的不一致。
其实使用defer时,用一个简单的转换规则改写一下,就不会迷糊了。改写规则是将return语句拆成两句写,return xxx会被改写成:
返回值 = xxx调用defer函数空的return
先看例1,它可以改写成这样:
func f() (result int) { result = 0 //return语句不是一条原子调用,return xxx其实是赋值+ret指令 func() { //defer被插入到return之前执行,也就是赋返回值和ret指令之间 result++ }() return}
所以这个返回值是1。
再看例2,它可以改写成这样:
func f() (r int) { t := 5 r = t //赋值指令 func() { //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过 t = t + 5 } return //空的return指令}
所以这个的结果是5。
最后看例3,它改写后变成:
func f() (r int) { r = 1 //给返回值赋值 func(r int) { //这里改的r是传值传进去的r,不会改变要返回的那个r值 r = r + 5 }(r) return //空的return}
所以这个例子的结果是1。
defer确实是在return之前调用的。但表现形式上却可能不像。本质原因是return xxx语句并不是一条原子指令,defer被插入到了赋值 与 ret之间,因此可能有机会改变最终的返回值。
defer的实现
defer关键字的实现跟go关键字很类似,不同的是它调用的是runtime.deferproc而不是runtime.newproc。
在defer出现的地方,插入了指令call runtime.deferproc,然后在函数返回之前的地方,插入指令call runtime.deferreturn。
普通的函数返回时,汇编代码类似:
add xx SPreturn
如果其中包含了defer语句,则汇编代码是:
call runtime.deferreturn,add xx SPreturn
goroutine的控制结构中,有一张表记录defer,调用runtime.deferproc时会将需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依次从defer表中出栈并执行。
------------------------------------------------
我在补充一下,go 的函数 带参数名的返回值和不带参数名的函数返回值对defer是有影响的。
如果defer 操作的是带参数的函数返回值的参数名,则会直接影响到函数的返回值;
如果defer操作的是函数内部的一个局部变量,这不会影响到函数的返回值;
原因很简单,return会做两个事情,
1.拷贝返回值到函数的返回值内存区,有如下几个场景:
- a.如果是函数的返回值带参数名(假设参数名为a),实际在函数内部对a的操作是直接操作这个返回值的内存区域;
如果函数返回直接调用return,不带任何返回值, 则不会有拷贝过程。此时defer如果没有显示的操作a,则不会影响到函数的返回值。
如果函数返回直接调用return X,则会有个a=x的拷贝过程,此时defer如果没有显示的操作a,也不会影响到函数的返回值(即使操作了x也不会影响结果。
- b如果是函数的返回值不带参名,函数的返回值需要一个显示的return x语句,此时会有一个拷贝过程,就是将x的值拷贝到返回值的内存区域,此时defer操作x 不会有什么副作用。
2.执行RET指令,执行跳转。
talk is cheep ,show me the code , 上一段代码:
package mainimport ("fmt")//f1 f2 f3 函数返回值带有参数名func f1() (r int) {defer func() {r++}()return 0}func f2() (r int) {defer func(i int) {i++}(r)return 0}func f3() (r int) {t := 0defer func() {t++}()return t}//f4 f5 函数的返回值都不带参数名,只有类型func f4() int {r := 0defer func() {r++}()return r}func f5() int {r := 0defer func(i int) {i++}(r)return 0}func main() {var v int = -1v = f1()fmt.Println("v=", v)v = f2()fmt.Println("v=", v)v = f3()fmt.Println("v=", v)v = f4()fmt.Println("v=", v)v = f5()fmt.Println("v=", v)}
结果:
v= 1v= 0v= 0v= 0v= 0
结论:为了避免defer可能引发的歧义,在定义函数时,最好使用不带参数名的方式。
引申一下,我们来看下go函数的栈帧结构,首先学习下几个寄存器
- FP 栈底寄存器,指向一个函数栈的顶部;
- PC 程序计数器,指向下一条执行指令;
- SB 指向静态数据的基指针,全局符号;
- SP 栈顶寄存器;
- golang-defer坑的本质
- Golang defer 使用时的坑
- golang的defer详解
- golang中defer的执行
- Golang有多个defer的情况
- golang中defer的详解
- Golang 中 defer 小坑
- golang defer
- golang 使用defer、panic、recover的问题
- Golang中defer、return、返回值之间执行顺序的坑
- Golang中defer、return、返回值之间执行顺序的坑
- golang中defer精析
- golang之defer
- golang之defer
- golang之defer
- golang defer 功能说明
- golang defer讲解
- 关于golang的defer、 return、返回值三者的执行逻辑
- git 入门使用
- PCA python代码实现
- 银行卡号的格式显示和获取数据
- 技术文章 | 合理分析竞争对手_有效提升关键词排名
- JavaSE (六)流程控制语句if、switch
- golang-defer坑的本质
- rownum用法详解
- select2 asp.net mvc jquery.validate.js jquery.validate.unobtrusive.js不能完美结合问题
- 前端试题
- EventBus的使用详解和封装
- 2017.8.29-9.4
- 当页面图片不存在时的处理方法
- MyBatis传入参数为list、数组、map写法
- [初学笔记] try...catch...end语句