本文节选自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
}

例2:

func f() (r int) {
t :=
defer func() {
t = t +
}()
return t
}

例3:

func f() (r int) {
defer func(r int) {
r = r +
}(r)
return
}

请读者先不要运行代码,在心里跑一遍结果,然后去验证。

例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 = //return语句不是一条原子调用,return xxx其实是赋值+ret指令
func() { //defer被插入到return之前执行,也就是赋返回值和ret指令之间
result++
}()
return
}

所以这个返回值是1。

再看例2,它可以改写成这样:

func f() (r int) {
t :=
r = t //赋值指令
func() { //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
t = t +
}
return //空的return指令
}

所以这个的结果是5。

最后看例3,它改写后变成:

func f() (r int) {
r = //给返回值赋值
func(r int) { //这里改的r是传值传进去的r,不会改变要返回的那个r值
r = r +
}(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 , 上一段代码:

  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. }

结果:

  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语句里面不应该有对命名返回值参数的操作。
 
 
 

【转】golang-defer坑的本质的更多相关文章

  1. 初生牛犊不怕虎 golang入坑系列

    读前必读,下面所有内容都是来自这里. 放到这里的目的,就是为了比对一下,哪里的读者多.平心而论,同样的Markdown,博客园排版真心X看,怎么瞅怎么X看.(X := '难' || X :='耐' | ...

  2. 听说,99% 的 Go 程序员都被 defer 坑过

    原文链接: 听说,99% 的 Go 程序员都被 defer 坑过 先声明:我被坑过. 之前写 Go 专栏时,写过一篇文章:Go 专栏|错误处理:defer,panic 和 recover.有小伙伴留言 ...

  3. golang defer那些坑

    defer以下几个特性,使用时需要关注下. 即时的参数传递 调用os.Exit()时defer不会被执行 defer与return的先后顺序 1.即时的参数传递 定义defer时传入的参数,是作为拷贝 ...

  4. golang初试:坑爷的

    用Golang与perl脚本比较, 初想至多差一倍吧...结果可不是一般的坑爹, 简直就是坑爷了. Perl脚本 #!/bin/bash source /etc/profile; function e ...

  5. 维多利亚的秘密 golang入坑系统

    原文在gitbook,字字原创,版权没有,转载随意. 在写本文的前一天,2017维密在上海开始了. 为了纪念屌丝界的盛世,特为本节起名维多利亚的秘密.现在的社会,要想出名只有抓眼球.所以写份技术文章, ...

  6. 崩溃 golang入坑系列

    早上(11.30)收到邮件,Vultr东京机房网络故障.当时搭建SS时,考虑到了机房故障.所以特意分出了日本和香港两条线路.但千算万算,忘记数据库还在东京机房中. 现在网络故障,SS服务器无法读取数据 ...

  7. 维多利亚的秘密 golang入坑系列

    原文在gitbook,字字原创,版权没有,转载随意. 在写本文的前一天,2017维密在上海开始了. 为了纪念屌丝界的盛世,特为本节起名维多利亚的秘密.现在的社会,要想出名只有抓眼球.所以写份技术文章, ...

  8. 准备冲锋 golang入坑系列

    史前摘要: 本来想写读前必读,但连续几篇博文都写读前必读,感觉就没有了新意. 所以换成史前摘要,反正是一个意思. 此摘要的目的仍然是提醒点击而来的同学,本系列最新文章在这里.放到博客园的目的是为了方便 ...

  9. 入坑第二式 golang入坑系统

    史前必读: 这是入坑系列的第二式,如果错过了第一式,可以去gitbook( https://andy-zhangtao.gitbooks.io/golang/content/ )点个回放,看个重播.因 ...

随机推荐

  1. C++内存管理2-内存泄漏

    1 C++中动态内存分配引发问题的解决方案 假设我们要开发一个String类,它可以方便地处理字符串数据.我们可以在类中声明一个数组,考虑到有时候字符串极长,我们可以把数组大小设为200,但一般的情况 ...

  2. Celery-系统守护进程

    1. 使用systemd控制Celery 用法: systemctl {start|stop|restart|status} celery.service 配置文件: /etc/celery/cele ...

  3. Docker容器部署Mysql数据库服务器

    本节会用到的Docker命令如下: docker images 查看mysql镜像id docker run -d -p xxxxxxxxxxxxxxx/zhufc/mysql:v2 通过镜像生成 启 ...

  4. go 代码玩耍

    //小游戏 num := rand.Intn() var input int fmt.Println("请输入数字") fmt.Scanf("%d", & ...

  5. Java分布式:分布式锁之Zookeeper

    Java分布式:分布式锁之Zookeeper 分布式锁系列教程重点分享锁实现原理 引入ZooKeeper ZooKeeper是什么呢? ZooKeeper 是一个开源的分布式协调服务,它可以在分布式系 ...

  6. Navicat连接Mysql8.0.11出现1251错误

    # 登录mysql mysql -u root -p # 修改加密规则 mysql> ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_pas ...

  7. Python窗体操作函数

    实现了一个window下对窗体操作的类,实现的功能如:移动窗体.获取窗体位置和大小.截取窗体图片.坐标转换等. 直接上代码: # coding=utf-8 import win32con import ...

  8. 第N个丑数

    #include <bits/stdc++.h> using namespace std; #define ll long long /* 把只包含质因子2.3和5的数称作丑数(Ugly ...

  9. JVM知识点总览-高级Java工程师面试必备

    jvm 总体梳理 jvm体系总体分四大块: 类的加载机制 jvm内存结构 GC算法 垃圾回收 GC分析 命令调优 当然这些知识点在之前的文章中都有详细的介绍,这里只做主干的梳理 这里画了一个思维导图, ...

  10. 【剑指offer】和为S的连续正数序列

    题目描述 小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100.但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数).没多久,他 ...