defer

defer是golang中使用的延迟调用的函数,该函数的使用场景就是如果函数执行出错(panic),也能够通过recover方式进行捕捉错误并将出错时的一些资源进行回收,如果在性能有要求的情况,并且错误能够控制的情况下还是直接避免使用该函数。

defer的使用场景描述

最理想情况下defer的性能对比
package mainimport ("testing"
)func test_defer(){defer func(){}()
}func test_normal(){func(){}()
}func BenchmarkNoDefer(b *testing.B) {for i := 0; i < b.N; i++ {test_normal()}
}func BenchmarkDefer(b *testing.B) {for i := 0; i < b.N; i++ {test_defer()}
}

对该段代码进行基准测试;

go test -bench=. deferw_test.go
goos: darwin
goarch: amd64
BenchmarkNoDefer-4      2000000000               1.21 ns/op
BenchmarkDefer-4        30000000                40.4 ns/op
PASS
ok      command-line-arguments  3.797s

发现如果该函数什么也不做的话,调用defer的情况比不调用的情况会有性能上的差距较大。

常用的文件操作defer基准测试
package mainimport ("fmt""os""testing"
)func test_defer(){file, err := os.Open("./test.txt")if err != nil {fmt.Println(err)return}defer file.Close()}func test_normal(){file, err := os.Open("./test.txt")if err != nil {fmt.Println(err)return}file.Close()
}func BenchmarkNoDefer(b *testing.B) {for i := 0; i < b.N; i++ {test_normal()}
}func BenchmarkDefer(b *testing.B) {for i := 0; i < b.N; i++ {test_defer()}
}

执行基准测试;

go test -bench=. deferw_test.go
goos: darwin
goarch: amd64
BenchmarkNoDefer-4        100000             17422 ns/op
BenchmarkDefer-4          100000             16553 ns/op
PASS
ok      command-line-arguments  3.789s

此时通过基准测试的输出,发现是否使用defer性能几乎相当,在此情况下,其实大部分的性能消耗都位于Open和Close的操作,使得defer在执行的过程中的性能占比很小几乎对整体性能没有影响。

defer捕获panic
package mainimport ("fmt"
)func test(){defer func(){if error := recover(); error != nil {fmt.Println("error ", error)}}()panic("raise error ")
}func main() {test()fmt.Println("over")
}

此时,输出的结果如下;

error  raise error
over

当不加defer中的recover时,此时程序就会报错退出,如果此时还有些需要回收的资源则不能释放,并且recover可以保证所在的协程能够继续运行下去。

defer的使用思考

defer的使用还是需要考虑到是否需要资源的回收,是否需要从异常中恢复或保存信息来做选择,如果在高并发的业务场景下并且当前场景下没有其他的耗时操作则可以考虑选择不用defer,一般在平常的场景下看个人的喜好来选择。

defer的执行过程

为什么在只有defer的操作过程中(本文第一个示例代码),添加了defer的操作性能会小于不添加defer的函数呢?接下来查看一下defer的背后到底做了什么工作。

示例代码
package mainimport "fmt"func main() {defer func(){fmt.Println("defer")}()}

进行反编译之后获取的指令如下;

  deferw.go:6           0x1092ee0               65488b0c2530000000      MOVQ GS:0x30, CX                        deferw.go:6           0x1092ee9               483b6110                CMPQ 0x10(CX), SP                       deferw.go:6           0x1092eed               764a                    JBE 0x1092f39                           deferw.go:6           0x1092eef               4883ec18                SUBQ $0x18, SP                          deferw.go:6           0x1092ef3               48896c2410              MOVQ BP, 0x10(SP)                       deferw.go:6           0x1092ef8               488d6c2410              LEAQ 0x10(SP), BP                       deferw.go:7           0x1092efd               c7042400000000          MOVL $0x0, 0(SP)                        deferw.go:7           0x1092f04               488d05cd9d0300          LEAQ go.func.*+125(SB), AX              deferw.go:7           0x1092f0b               4889442408              MOVQ AX, 0x8(SP)                        deferw.go:7           0x1092f10               e8eb3bf9ff              CALL runtime.deferproc(SB)              deferw.go:7           0x1092f15               85c0                    TESTL AX, AX                            deferw.go:7           0x1092f17               7510                    JNE 0x1092f29                           deferw.go:11          0x1092f19               90                      NOPL                                    deferw.go:11          0x1092f1a               e87144f9ff              CALL runtime.deferreturn(SB)            deferw.go:11          0x1092f1f               488b6c2410              MOVQ 0x10(SP), BP                       deferw.go:11          0x1092f24               4883c418                ADDQ $0x18, SP                          deferw.go:11          0x1092f28               c3                      RET                                     deferw.go:7           0x1092f29               90                      NOPL                                    deferw.go:7           0x1092f2a               e86144f9ff              CALL runtime.deferreturn(SB)            deferw.go:7           0x1092f2f               488b6c2410              MOVQ 0x10(SP), BP                       deferw.go:7           0x1092f34               4883c418                ADDQ $0x18, SP                          deferw.go:7           0x1092f38               c3                      RET                                     deferw.go:6           0x1092f39               e872c2fbff              CALL runtime.morestack_noctxt(SB)       deferw.go:6           0x1092f3e               eba0                    JMP main.main(SB)

从执行的流程可知首先会调用deferproc来创建defer;

func deferproc(siz int32, fn *funcval) { // arguments of fn follow fnif getg().m.curg != getg() {// go code on the system stack can't deferthrow("defer on system stack")}// the arguments of fn are in a perilous state. The stack map// for deferproc does not describe them. So we can't let garbage// collection or stack copying trigger until we've copied them out// to somewhere safe. The memmove below does that.// Until the copy completes, we can only call nosplit routines.sp := getcallersp()argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)   // 获取参数的起始地址callerpc := getcallerpc()      // 获取定义的函数的位置d := newdefer(siz)            // 生成一个新的defer结构if d._panic != nil {throw("deferproc: d.panic != nil after newdefer")}d.fn = fn                     // 设置该接口的执行函数d.pc = callerpc                          // 调用的pc地址d.sp = spswitch siz {case 0:// Do nothing.case sys.PtrSize:*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))default:memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))       }// deferproc returns 0 normally.// a deferred func that stops a panic// makes the deferproc return 1.// the code the compiler generates always// checks the return value and jumps to the// end of the function if deferproc returns != 0.return0()// No code can go here - the C return register has// been set and must not be clobbered.
}

其中主要就是通过newdefer来创建一个defer,

//go:nosplit
func newdefer(siz int32) *_defer {var d *_defersc := deferclass(uintptr(siz))gp := getg()                                             // 获取当前的协程if sc < uintptr(len(p{}.deferpool)) {     // 是否小于deferpoolpp := gp.m.p.ptr()if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {// Take the slow path on the system stack so// we don't grow newdefer's stack.systemstack(func() {lock(&sched.deferlock)           // 复用deferfor len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {d := sched.deferpool[sc]sched.deferpool[sc] = d.link    // 保存d的linkd.link = nilpp.deferpool[sc] = append(pp.deferpool[sc], d)  // 将当前的d 添加到deferpool中}unlock(&sched.deferlock)})}if n := len(pp.deferpool[sc]); n > 0 {       // 重新初始化deferpool队列d = pp.deferpool[sc][n-1]pp.deferpool[sc][n-1] = nilpp.deferpool[sc] = pp.deferpool[sc][:n-1]}}if d == nil {                                                              // 如果没有找到则创建一个新的defer// Allocate new defer+args.systemstack(func() {total := roundupsize(totaldefersize(uintptr(siz)))d = (*_defer)(mallocgc(total, deferType, true))        // 申请内存获取d的空间大小})if debugCachedWork {// Duplicate the tail below so if there's a// crash in checkPut we can tell if d was just// allocated or came from the pool.d.siz = sizd.link = gp._defer                                                                // 将协程的_defer保存到d的link上gp._defer = d                                                                       // 设置新的_defer为当前的dreturn d}}d.siz = sizd.link = gp._defergp._defer = dreturn d
}

从新建的流程可知,通过协程的_defer来保存该协程中所有的_defer,如果有新增则将新增的添加到头部,通过这么一个链表来完成defer的先入后执行。

当执行到runtime.deferreturn时,就会触发defer的执行。

//go:nosplit
func deferreturn(arg0 uintptr) {gp := getg()d := gp._defer            // 获取当前协程的_defer链表if d == nil {return                             // 如果为空则返回}sp := getcallersp()if d.sp != sp {return}// Moving arguments around.//// Everything called after this point must be recursively// nosplit because the garbage collector won't know the form// of the arguments until the jmpdefer can flip the PC over to// fn.switch d.siz {case 0:// Do nothing.case sys.PtrSize:*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))default:memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))}fn := d.fn                                // 获取当前的fnd.fn = nil                               // 将当前fn置空gp._defer = d.link           // 获取下一个_defer并设置到_defer中freedefer(d)                           // 释放内容 jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))   // 执行fn函数
}

至此,defer的函数的执行过程就大致执行完成。

总结

defer的使用场景根据需要自行选择,如果在高并发的情况下,还是尽量少使用defer的调用,如果在有出错的情况下且有其他资源需要管理的情况下,建议使用defer来控制资源的回收或释放,并且defer的执行链路都是通过协程来执行的,所以defer执行的过程中要注意是否跨了协程来操作了其他的资源,可能会达不到defer先入后出的效果。由于本人才疏学浅,如有错误请批评指正。

golang源码分析:defer流程分析相关推荐

  1. Android 源码 PackageManagerService 启动流程分析

    <Android 源码 installPackage 流程分析>一节着重分析了 apk 安装流程,接下来我们分析 PackageManagerService 启动时都做了些什么? 执行 P ...

  2. Mybatis源码之核心流程分析

    终于谈到了Mybatis最核心的东西了,最核心的就是通过配置XML文件或注解中的SQL,直接调用接口就能执行配置好的SQL语句并封装成对应的返回类型的数据. 先看一下Mybatis使用示例: //创建 ...

  3. Android4.0源码Launcher启动流程分析【android源码Launcher系列一】

    最近研究ICS4.0的Launcher,发现4.0和2.3有稍微点区别,但是区别不是特别大,所以我就先整理一下Launcher启动的大致流程. Launcher其实是贯彻于手机的整个系统的,时时刻刻都 ...

  4. Android中ICS4.0源码Launcher启动流程分析【android源码Launcher系列一】

    最近研究ICS4.0的Launcher,发现4.0和2.3有稍微点区别,但是区别不是特别大,所以我就先整理一下Launcher启动的大致流程.Launcher其实是贯彻于手机的整个系统的,时时刻刻都在 ...

  5. Android 源码 Wi-Fi 连接流程分析

    Wi-Fi 连接过程可以从 Settings App 中点击任意 Wi-Fi 条目连接说起.点击条目以后会弹出一个对话框,根据不同的 Wi-Fi 类型需要填入必要的信息,再点击连接按钮,发起连接过程. ...

  6. libevent源码学习-----事件驱动流程分析

    libevent中事件驱动的大体流程如下 /* 创建事件驱动 */ struct event_base* base = event_base_new(); /**创建一个事件*@param base: ...

  7. golang源码分析-启动过程概述

    golang源码分析-启动过程概述 golang语言作为根据CSP模型实现的一种强类型的语言,本文主要就是通过简单的实例来分析一下golang语言的启动流程,为深入了解与学习做铺垫. golang代码 ...

  8. 【我的架构师之路】- golang源码分析之协程调度器底层实现( G、M、P)

    本人的源码是基于go 1.9.7 版本的哦! 紧接着之前写的 [我的区块链之路]- golang源码分析之select的底层实现 和 [我的区块链之路]- golang源码分析之channel的底层实 ...

  9. golang源码分析-调度概述

    golang源码分析-调度过程概述 本文主要概述一下golang的调度器的大概工作的流程,众所周知golang是基于用户态的协程的调度来完成多任务的执行.在Linux操作系统中,以往的多线程执行都是通 ...

  10. 【Android 启动过程】Activity 启动源码分析 ( ActivityThread 流程分析 二 )

    文章目录 前言 一.ActivityManagerService.attachApplicationLocked 二.ActivityStackSupervisor.attachApplication ...

最新文章

  1. wamp安装和配置_Joomla安装教程
  2. 安装的 Python 版本太多互相干扰?pyenv 建议了解一下。
  3. mysql 表名不加单引号_当表名“ match”没有用单引号引起来时,MySQL引发错误?...
  4. html游戏禁止微信浏览器下拉,如何用电脑模拟微信浏览器浏览禁止PC打开的微网站...
  5. assets目录与res/raw目录下文件的区别
  6. 干货 | 鸟瞰 MySQL,唬住面试官!
  7. 基于Keras+YOLOv3的口罩佩戴情况检测系统【超详细!!!保姆级教程】
  8. Spring事务管理A方法内部调用B方法的回滚问题(springboot事务管理)
  9. 深入理解Moya设计
  10. 教你一招恢复100分信用分,新手违规被扣40分,还有救吗?
  11. 安卓手机的指纹存储在手机内部有没有可能被窃取?
  12. OpenGL学习笔记(3)之渲染管线
  13. 【SVN】新旧服务器更替,完成svn服务器迁移
  14. 【Linux基础】Ubuntu 20.04系统安装(完整版)
  15. UART 通用串行通信整理
  16. Python实现中英文翻译脚本
  17. Redis 之 subscribe 订阅模式封装
  18. keepalived简易安装及配置文件详解
  19. 5g工业路由器的电梯远程监测管理应用
  20. CES 2023:高通从移动互联深入布局未来智驾

热门文章

  1. Java架构技术文档:并发编程+设计模式+常用框架+JVM+精选视频
  2. 转型AI成功几率有几分?太真实了......
  3. AI落地遭“卡脖子”困境:为什么说联邦学习是解决良方?
  4. 不甘心只做输入工具,搜狗输入法上线AI助手,提供智能服务
  5. 腾讯 AI Lab 开源业内最大规模多标签图像数据集
  6. 免费公开课 | 机器学习的第二次入门
  7. PRICAI开幕 第四范式发起AutoML议题
  8. 打一场AI竞赛,让你知道我的厉害
  9. MySQL 批量插入:如何不插入重复数据?
  10. Java中的深浅拷贝问题你清楚吗?