当前位置: 首页 > news >正文

【go】defer底层原理

defer的作用

defer声明的函数在当前函数return之后执行,通常用来做资源、连接的关闭和缓存的清除等。

A defer statement pushes a function call onto a list. The list of saved calls is executed after the surrounding function returns. Defer is commonly used to simplify functions that perform various clean-up actions.

注意:函数的return其实分为两部分:1将要返回的值赋值给返回值地址空间,2返回返回地址空间中的值。而defer发生在这两个操作中间,会造成一些defer不生效的错觉。详见go函数调用

defer原理

使用defer关键字声明的方法,实际上编译器会转换为两个方法的调用:

  • runtime.deferproc:将defer函数注册到goroutine中
  • runtime.deferreturn:在外层函数的末尾添加,执行注册的defer

我们都知道注册了多个defer函数后,defer的执行顺序是倒序的,类似栈的后进先出特性,实际上go使用链表实现的defer,每次新注册的defer都是头插法注册到链表中:

// 移除了无关代码
func deferproc(fn func()) {//获取当前ggp := getg()//新建defer结构d := newdefer()//将新创建的d添加到defer链表头d.link = gp._defergp._defer = dd.fn = fn
}
// defer结构体中有指向下一个defer的指针
type _defer struct {started bool	//defer是否开始执行heap    bool	//是否堆分配openDefer bool	//是否是open codedsp        uintptr // sp at time of deferpc        uintptr // pc at time of deferfn        func()  // can be nil for open-coded defers_panic    *_panic // 触发当前defer的paniclink      *_defer // defer链表fd   unsafe.Pointer // funcdata for the function associated with the framevarp uintptr        // value of varp for the stack frameframepc uintptr
}
// g结构体持有defer链表的头指针
type g struct{_defer    *_defer
}

嵌套defer

嵌套的defer依然按照顺序来看:先注册A中的A1,然后defer注册B,B中的defer顺序为B1->B2,最后注册A2。现在defer链表中是这样的:A2->B2->B1->A1。

func A() {defer A1()defer B()defer A2()
}
func B() {defer B1()defer B2()
}
func A1() {fmt.Println("defer A1")
}
func A2() {fmt.Println("defer A2")
}
func B1() {fmt.Println("defer B1")
}
func B2() {fmt.Println("defer B2")
}
func main() {A()
}//输出
//defer A2
//defer B2
//defer B1
//defer A1

defer与panic

GO语言使用error机制来表示程序异常,而panic一般用于表示严重的错误出现。当发生panic时,如果没有使用recover来捕捉panic,那么程序就会退出。我们在日常开发中一般不会使用panic,因为大多数时候我们都希望程序能够运行下去,而不是直接停止服务。

当发生panic时,panic之后的代码不会执行,但是会触发之前注册的defer:

func A() {defer A1()panic("panic A")defer A3()
}
func A1() {fmt.Println("defer A1")
}
func A3() {fmt.Println("defer A3")
}
func main() {A()
}
/** 输出结果
defer A1
panic: panic A
**/

可以看到发生panicA时,触发了A中已经注册的defer A1和A2,但是A3还未注册,所以不会执行。在同一个方法里只会有一个panic生效,因为后续代码不会执行了。

panic结构体

type _panic struct {argp      unsafe.Pointer // 当前panic要执行的defer函数参数arg       any            // panic的参数link      *_panic        // link to earlier panicpc        uintptr        // where to return to in runtime if this panic is bypassedsp        unsafe.Pointer // where to return to in runtime if this panic is bypassedrecovered bool           // panic是否被恢复aborted   bool           // panic是否终止goexit    bool
}
// g中也保存panic指针
type g struct{_panic    *_panic
}

当发生一个panic时,当前的panic会被加入g的panic链表。然后执行defer链表。

多个panic和defer嵌套

func A() {defer A1()defer A2()panic("panic A")
}
func A1() {fmt.Println("defer A1")panic("panic A1")
}
func A2() {fmt.Println("defer A2")
}
func main() {A()
}
/**
defer A2
defer A1
panic: panic Apanic: panic A1
**/        

这段代码中的执行逻辑:

  1. 注册defer A1和A2,现在的defer链表是:A2->A1
  2. 触发panic A,将panic A加入panic链表头
  3. 执行defer链表,正常执行defer A2,然后将A2移出defer链表
  4. 执行defer A1,此时A1中又触发了panic,不能正常执行完成,将A1的_defer.started标记为true,并将_defer._panic记为panic A。此时defer链表中只有一个started为true的defer A1
  5. 将新触发的panic A1加入到panic链表头,然后去执行defer链表,发现A1的started为true,且_panic是panic A。将panic A的aborted设为true,标记为终止。同时移除defer A1。
  6. 打印panic信息,从链表尾开始,panic A->panic A1

有recover的情况

recover的工作只是将panic的recovered字段设置为true。
先看一个简单的例子:

func A() {defer A1()defer A2()panic("panic A")
}
func A1() {fmt.Println("defer A1")
}
func A2() {if err := recover(); err != nil {fmt.Println(err)}fmt.Println("defer A2")
}
func main() {A()
}/**
输出结果
panic A
defer A2
defer A1
**/

这段代码的执行逻辑:

  1. 注册defer A1和A2
  2. 运行到panic时,将panic A加入defer链表。然后执行defer链表,先执行A2
  3. 执行A2时触发了recover,将panic A的recovered字段设为true,执行recover分支中的打印"panic A",然后继续执行A2中的打印语句"defer A2"
  4. 在defer A2执行完成后,go会检查当前panic已经recovered了,于是将panic A移出panic链表,然后通过_defer.sp和_defer.pc两个指针来找到要跳转的指令,最后将defer A2移出链表。
  5. 根据sp和pc跳回到defer执行逻辑,继续执行defer A1,打印出"defer A1"

多个panic

func A() {defer A2()defer A1()panic("panic A")
}
func A1() {fmt.Println("defer A1")panic("panic A1")
}
func A2() {if err := recover(); err != nil {fmt.Println(err)}fmt.Println("defer A2")
}
func main() {A()
}
/**
输出:
defer A1
panic A1
defer A2
**/

上面的代码在defer A1中也抛出了panic,多个panic同时存在时,recover只会捕捉到最新的。也只需要捕获一次。

recover之后又发生了panic

func A() {defer A2()defer A1()panic("panic A")
}
func A1() {fmt.Println("defer A1")
}
func A2() {if err := recover(); err != nil {fmt.Println(err)}fmt.Println("defer A2")panic("panic A1")
}
func main() {A()
}
/**
输出:
defer A1
panic A
defer A2
panic: panic A [recovered]panic: panic A1
**/

先触发的panic A被recover捕获到,打印的时候会标识为[recovered],再次触发的panic A1由于后续没有recover处理,所以程序终止。

defer的逐步优化

1.12版本中的defer是在堆中分配的,虽然golang实现了defer池可以服用defer结构体,但是效率依然不理想。在1.13版本中加入了defer栈上分配的功能,这样defer的性能就快很多。

func deferprocStack(d *_defer) {gp := getg()if gp.m.curg != gp {// go code on the system stack can't deferthrow("defer on system stack")}// fn is already set.// The other fields are junk on entry to deferprocStack and// are initialized here.d.started = falsed.heap = falsed.openDefer = falsed.sp = getcallersp()d.pc = getcallerpc()d.framepc = 0d.varp = 0// The lines below implement://   d.panic = nil//   d.fd = nil//   d.link = gp._defer//   gp._defer = d// But without write barriers. The first three are writes to// the stack so they don't need a write barrier, and furthermore// are to uninitialized memory, so they must not use a write barrier.// The fourth write does not require a write barrier because we// explicitly mark all the defer structures, so we don't need to// keep track of pointers to them with a write barrier.*(*uintptr)(unsafe.Pointer(&d._panic)) = 0*(*uintptr)(unsafe.Pointer(&d.fd)) = 0*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))return0()// No code can go here - the C return register has// been set and must not be clobbered.
}

1.14中加入了open coded defer,这使得我们在使用defer时不一定要创建defer结构体并加入到defer链表中,而是直接在编译阶段在函数体内加上相应的逻辑。

参考B站幼麟实验室

http://www.lryc.cn/news/15411.html

相关文章:

  • TypeScript 学习笔记
  • 【C++】map和set的使用
  • 微电影广告具有哪些特点?
  • Android RxJava框架源码解析(四)
  • Linux信号-进程退出状态码
  • springcloud+vue实现图书管理系统
  • GEE学习笔记 六十:GEE中生成GIF动画
  • react中的useEffect
  • 故障安全(Crash-Safe) 复制
  • Spring aop之针对注解
  • 【JavaScript速成之路】JavaScript数据类型转换
  • 21-绑定自定义事件
  • 【Mysql】触发器
  • CODESYS开发教程11-库管理器
  • 【UnityAR相关】Unity Vuforia扫图片成模型具体步骤
  • 2023年全国最新保安员精选真题及答案2
  • keil5安装了pack包但是还是不能选择device
  • 秒杀系统设计
  • 全面认识数据指标体系
  • 热榜首推!阿里内部都在用的Java后端面试笔记,主流技术全在里面了!备战2023Java面试,拿理想offer
  • Android架构设计——【 APT技术实现butterknife框架 】
  • 线程的基本概念
  • java面试题中常见名词注解
  • SpringAOP从入门到源码分析大全,学好AOP这一篇就够了(二)
  • 华为OD机试 - 斗地主(C++) | 附带编码思路 【2023】
  • 【存储】etcd的存储是如何实现的(3)-blotdb
  • 基于MATLAB开发AUTOSAR软件应用层模块-part21.SR interface通信介绍(包括isupdated判断通信)
  • Kotlin新手教程八(泛型)
  • 性能测试知多少?怎样开展性能测试
  • code-breaking之javacon