本文节选自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 := 5defer 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 := 5r = 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 SP
return

如果其中包含了defer语句,则汇编代码是:

call runtime.deferreturn,
add xx SP
return

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 不会有什么副作用,此时存放返回值的内存区域是个匿名区域,从源程序的角度看defer直接操作变量是不可能访问到这个区域。可见不带形参的返回是避免defer副作用的最有效手段。

2、执行RET指令,执行跳转。

talk is cheep ,show me the code , 上一段代码:

[csharp] view plain copy

 

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. //f1 f2 f3 函数返回值带有参数名
  6. func f1() (r int) {
  7. defer func() {
  8. r++
  9. }()
  10. return 0
  11. }
  12. func f2() (r int) {
  13. defer func(i int) {
  14. i++
  15. }(r)
  16. return 0
  17. }
  18. func f3() (r int) {
  19. t := 0
  20. defer func() {
  21. t++
  22. }()
  23. return t
  24. }
  25. //f4 f5 函数的返回值都不带参数名,只有类型
  26. func f4() int {
  27. r := 0
  28. defer func() {
  29. r++
  30. }()
  31. return r
  32. }
  33. func f5() int {
  34. r := 0
  35. defer func(i int) {
  36. i++
  37. }(r)
  38. return 0
  39. }
  40. func main() {
  41. var v int = -1
  42. v = f1()
  43. fmt.Println("v=", v)
  44. v = f2()
  45. fmt.Println("v=", v)
  46. v = f3()
  47. fmt.Println("v=", v)
  48. v = f4()
  49. fmt.Println("v=", v)
  50. v = f5()
  51. fmt.Println("v=", v)
  52. }

结果:

[csharp] view plain copy

 

  1. v= 1
  2. v= 0
  3. v= 0
  4. v= 0
  5. v= 0

结论:为了避免defer可能引发的歧义,在定义函数时,最好使用不带参数名的方式。

引申一下,我们来看下go函数的栈帧结构,首先学习下几个寄存器

  1. FP 栈底寄存器,指向一个函数栈的顶部;
  2. PC   程序计数器,指向下一条执行指令;
  3. SB 指向静态数据的基指针,全局符号;
  4. SP 栈顶寄存器;
 

1.go的函数调用现场清理由主调函数负责维护;
2.函数的返回地址也在主调函数的栈上开辟;
3.栈上的数据是通过FB+- 偏移量来操作;
4.return语句分两部分执行:
  把返回值拷贝到返回值的栈上空间内
  调用RET指令执行函数调用后的下一条指令
但是return的语句不是原子的,defer语句的执行被插入在这两条指令执行之间,这是导致defer导致歧义的最根本原因。为避免困惑,
defer 不应该操作函数返回值存放的区域,即defer语句里面不应该有对命名返回值参数的操作。
 

 

转载于:https://www.cnblogs.com/ralap7/p/9194734.html

【转】golang-defer坑的本质相关推荐

  1. Golang单元测试坑盘点

    Golang单元测试坑盘点 最近在公司写单元测试,发现了不少坑.例如:monkey不支持inline函数,vscode单测有缓存,convey对切片.map等比较不可以直接使用类似于==进行.本节呢, ...

  2. golang defer实现

    golang defer实现 1.现象 defer 会在函数return前执行 如果发生非系统级别panic,defer依然执行:在defer执行过程中,如果有recover,那就捕获panic,否则 ...

  3. Golang defer 快速上手

    文章目录 1.简介 2.注意事项 2.1 defer 函数入参在 defer 时确定 2.2 defer 执行顺序为后进先出 2.3 defer 函数在 return 语句赋值与返回之间执行 2.4 ...

  4. Golang defer解读

    defer defer是Go语言提供的一种用于注册延迟调用的机制:让函数或语句可以在当前函数执行完毕后(包括通过return正常结束或者panic导致的异常结束)执行. defer语句通常用于一些成对 ...

  5. golang defer简介 goland 警告提示 possible resource leak,difer is called in a for loop 原因

    目录 警告原因 解决方法 defer理解 defer调用是一个栈结构 defer的作用域是一个函数,不是一个语句块 链式调用 针对非指针类型调用函数 警告原因 在for中使用defer关闭资源,其实资 ...

  6. golang defer的使用

    在golang当中,defer代码块会在函数调用链表中增加一个函数调用.这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之后添加一个函数调用.因此,defer通常用来释放函数内 ...

  7. golang 踩坑:xml解析问题,chanel使用问题

    最近在用golang,记录一下印象比较深的坑 1.第一个是xml解析时的结构体关键字首字母一定要大写,之前解析xml一直解析不了,我一直以为是我结构体构建有问题,对来对去都看不出问题,最后才在一篇不显 ...

  8. Golang defer

    在Golang使用defer常常会迷惑于以下两个问题 defer 关键字的调用时机以及多次调用 defer 时执行顺序是如何确定的: defer 关键字使用传值的方式传递参数时会进行预计算,导致不符合 ...

  9. golang defer 关闭文件 报错file may have nil or other unexpected value as its corresponding error

    错误实例: file, err := os.Open("xxx.txt") defer file.Close() if err != nil {return err } 初学者很多 ...

最新文章

  1. Swift 5 发布,ABI 终于稳定了
  2. 第三次作业——(涂江凤、邓洪虹)
  3. Linux的企业-Mfs高可用corosync+pacemaker+fence+iscci
  4. IBM HyperLedger fabric 详解
  5. mysql兼容性高吗_MySQL与标准的兼容性
  6. flex实现水平居中和两栏布局
  7. java栈API_Java中的堆栈API——Stack
  8. JavaScript玩转机器学习:保存并加载 tf.Model
  9. Mysql学习总结(18)——Mysql主从架构的复制原理及配置详解
  10. codeforces 768 C. Jon Snow and his Favourite Number(思维+暴力)
  11. requests 上传本地文件到服务器
  12. 诡异的 com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column “dat
  13. C#制作网站挂机程序
  14. Sniffer报文捕获解析
  15. 在Windows 10中截取截图的6种方式 简介
  16. php怎么实现根据图片搜索图片功能
  17. MATLAB用rbf拟合sinx,为什么径向基(RBF)网络拟合度很差呢?
  18. Mac os下快速从终端进入Finder 从Finder进入终端
  19. redis脚本将hgetall转为json
  20. 【华为机试真题 Python实现】篮球比赛【2022 Q1 Q2 | 200分】

热门文章

  1. Go gin参数绑定
  2. linux主流文件系统及日志和非日志文件系统的区别
  3. Mac brew安装maven
  4. 阿里巴巴矢量图标库iconfont使用教程
  5. linux ubuntu/deepin安装tengine(nginx增强版)2.3.2
  6. linux centos7完整记录对根目录的磁盘扩容过程(扩容99G)
  7. Golang管道channel:管道的声明 读取 写入
  8. Scala泛型:协变和逆变
  9. Spring @ConfigurationProperties注解使用示例
  10. Python字符串前加u/r/b的作用