当前位置: 首页 > 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站幼麟实验室

相关文章:

【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 u…...

TypeScript 学习笔记

最近在学 ts 顺便记录一下自己的学习进度,以及一些知识点的记录,可能不会太详细,主要是用来巩固和复习的,会持续更新 前言 想法 首先我自己想说一下自己在学ts之前,对ts的一个想法和印象,在我学习之前&a…...

【C++】map和set的使用

map和set一、set1.1 set的介绍1.2 set的使用1.2.1 set的构造1.2.2 set的迭代器1.2.3 set的修改1.2.3.1 insert && find && erase1.2.3.2 count1.3 multiset二、map2.1 map的介绍2.2 map的使用2.2.1 map的修改2.2.1.1 insert2.2.1.2 统计次数2.3 multimap一、se…...

微电影广告具有哪些特点?

微电影广告是广告主投资的,以微电影为形式载体,以新媒体为主要传播载体,综合运用影视创作手法拍摄的集故事性、艺术性和商业性于一体的广告。它凭借精彩的电影语言和强大的明星效应多渠道联动传播,润物细无声地渗透和传递着商品信…...

Android RxJava框架源码解析(四)

目录一、观察者Observer创建过程二、被观察者Observable创建过程三、subscribe订阅过程四、map操作符五、线程切换原理简单示例1&#xff1a; private Disposable mDisposable; Observable.create(new ObservableOnSubscribe<String>() {Overridepublic void subscribe(…...

Linux信号-进程退出状态码

当进程因收到信号被终止执行退出后&#xff0c;父进程可以通过wait或waitpid得到它的exit code。进程被各信号终止的退出状态码总结如下&#xff1a;信号编号信号名称信号描述默认处理方式Exit code1SIGHUP挂起终止12SIGINT终端中断终止23SIGQUIT终端退出终止、coredump1314SIG…...

springcloud+vue实现图书管理系统

一、前言&#xff1a; 今天我们来分享一下一个简单的图书管理系统 我们知道图书馆系统可以有两个系统&#xff0c;一个是管理员管理图书的系统&#xff0c;管理员可以&#xff08;1&#xff09;查找某一本图书情况、&#xff08;2&#xff09;增加新的图书、&#xff08;3&…...

GEE学习笔记 六十:GEE中生成GIF动画

生成GIF动画这个是GEE新增加的功能之一&#xff0c;这一篇文章我会简单介绍一下如何使用GEE来制作GIF动画。 相关API如下&#xff1a; 参数含义&#xff1a; params&#xff1a;设置GIF动画显示参数&#xff0c;详细的参数可以参考ee.data.getMapId() callback&#xff1a;回调…...

react中的useEffect

是函数组件中执行的副作用&#xff0c;副作用就是指每次组件更新都会执行的函数&#xff0c;可以用来取代生命周期。 1. 基本用法 import { useEffect } from "react"; useEffect(()>{console.log(副作用); });2. 副作用分为需要清除的和不需要清除 假如设置…...

故障安全(Crash-Safe) 复制

二进制日志记录是故障安全的:MySQL 仅记录完成的事件或事务使用 sync-binlog 提高安全性默认值是1&#xff0c;最安全的&#xff0c;操作系统在每次事务后写入文件将svnc-binloq 设置为0&#xff0c;当操作系统根据其内部规则写入文件的同时服务器崩溃时性能最好但事务丢失的可…...

Spring aop之针对注解

前言 接触过Spring的都知道&#xff0c;aop是其中重要的特性之一。笔者在开发做项目中&#xff0c;aop更多地是要和注解搭配&#xff1a;在某些方法上加上自定义注解&#xff0c;然后要对这些方法进行增强(很少用execution指定&#xff0c;哪些包下的哪些方法要增强)。那这时就…...

【JavaScript速成之路】JavaScript数据类型转换

&#x1f4c3;个人主页&#xff1a;「小杨」的csdn博客 &#x1f525;系列专栏&#xff1a;【JavaScript速成之路】 &#x1f433;希望大家多多支持&#x1f970;一起进步呀&#xff01; 文章目录前言数据类型转换1&#xff0c;转换为字符串型1.1&#xff0c;利用“”拼接转换成…...

21-绑定自定义事件

绑定自定义事件 利用自定义事件获取子组件的值 父组件给子组件绑定一个自定义事件&#xff0c;实际上是绑定到了子组件的实例对象vc上&#xff1a; <!-- 自定义myEvent事件 --> <Student v-on:myEventgetStudentName/>在父组件中编写getStudentName的实现&#…...

【Mysql】触发器

【Mysql】触发器 文章目录【Mysql】触发器1. 触发器1.1 介绍1.2 语法1.2.1 创建触发器1.2.2 查看触发器1.2.3 删除触发器1.2.4 案例1. 触发器 1.1 介绍 触发器是与表有关的数据库对象&#xff0c;指在insert、update、delete之前(BEFORE)或之后(AFTER)&#xff0c;触发并执行…...

CODESYS开发教程11-库管理器

今天继续我们的小白教程&#xff0c;老鸟就不要在这浪费时间了&#x1f60a;。 前面一期我们介绍了CODESYS的文件读写函数库SysFile。大家可能发现了&#xff0c;在CODESYS的开发中实际上是离不开各种库的使用&#xff0c;其中包括系统库、第三方库以及用户自己开发的库。实际…...

【UnityAR相关】Unity Vuforia扫图片成模型具体步骤

1 资产准备 导入要生成的fbx模型&#xff08;带有材质&#xff09;&#xff0c; 你会发现导入fbx的材质丢失了&#xff1a; 选择Standard再Extract Materials导出材质到指定文件夹下&#xff08;我放在Assets->Materials了 ok啦&#xff01; 材质出现了&#xff0c; 模型…...

2023年全国最新保安员精选真题及答案2

百分百题库提供保安员考试试题、保安职业资格考试预测题、保安员考试真题、保安职业资格证考试题库等&#xff0c;提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 21.一般来说&#xff0c;最经济的巡逻方式是&#xff08;&#xff09;。 A:步巡 B:…...

keil5安装了pack包但是还是不能选择device

一开始&#xff0c;我以为是keil5无法安装 STM32 芯片包&#xff0c;打开device倒是可以看到stm公司的芯片包&#xff0c;但是没有我想要的stm32f1。 我按照网上的一些说法&#xff0c;找到了这个STM32F1 的pack芯片包&#xff0c;但是我双击安装的时候&#xff0c;它的安装位…...

秒杀系统设计

1.秒杀系统的特点 瞬时高并发 2.预防措施 2.1.流量限制 对于一个相同的用户&#xff0c;限制请求的频次对于一个相同的IP&#xff0c;限制请求的频次验证码&#xff0c;减缓用户请求的次数活动开启之前&#xff0c;按钮先置灰&#xff0c;防止无效的请求流入系统&#xff0…...

全面认识数据指标体系

什么是数据指标体系&#xff1f; 看了下百度百科&#xff0c;竟然没有数据指标这个词条&#xff0c;看来这个词大家平时还用的不多啊。那只有间接偷懒一下&#xff0c;分别查下指标和数据这两个词条的含义&#xff0c;在组合起来看看。 数据&#xff1a;数据是指对客观事件进…...

热榜首推!阿里内部都在用的Java后端面试笔记,主流技术全在里面了!备战2023Java面试,拿理想offer

纵观今年的技术招聘市场&#xff0c; Java依旧是当仁不让的霸主 &#xff01;即便遭受 Go等新兴语言不断冲击&#xff0c;依旧岿然不动。究其原因&#xff1a;Java有着极其成熟的生态&#xff0c;这个不用我多说&#xff1b;Java在 运维、可观测性、可监 控性方面都有着非常优秀…...

Android架构设计——【 APT技术实现butterknife框架 】

APT简介 APT英文全称&#xff1a;Android annotation process tool是一种处理注释的工具&#xff0c;它对源代码文件进行检测找出其中的Annotation&#xff0c;使用Annotation进行额外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文…...

线程的基本概念

文章目录基础概念线程与进程什么是进程&#xff1f;什么是线程&#xff1f;进程和线程的区别&#xff1a;多线程什么是多线程&#xff1f;多线程的局限性串行、并行、并发同步异步、阻塞非阻塞线程的创建1、继承Thread类&#xff0c;重写run方法2、实现Runnable接口&#xff0c…...

java面试题中常见名词注解

一.常见名词注解 1.mysql索引&#xff0c;索引数据结构&#xff0c;hash&#xff0c;二叉树&#xff0c;B树&#xff0c;B树&#xff0c;红黑树&#xff0c; mysql索引&#xff1a;帮助mysql高效获取数据的数据结构&#xff0c;通俗来说&#xff0c;数据库索引就好比一本书的…...

SpringAOP从入门到源码分析大全,学好AOP这一篇就够了(二)

文章目录系列文档索引四、Spring AOP的使用入门1、激活AspectJ模块&#xff08;1&#xff09;注解激活&#xff08;2&#xff09;XML激活2、创建 AspectJ 代理&#xff08;了解&#xff09;&#xff08;1&#xff09;编程方式创建 AspectJ 代理实例&#xff08;2&#xff09;XM…...

华为OD机试 - 斗地主(C++) | 附带编码思路 【2023】

刷算法题之前必看 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,通过率才会高。 华为 OD 清单查看地址:https://blog.csdn.net/hihell/category_12199283.html 华为OD详细说明:https://dream.blog.csdn.net/article/details/128980730 华为OD机试题…...

【存储】etcd的存储是如何实现的(3)-blotdb

前两篇分别介绍了etcd的存储模块以及mvcc模块。在存储模块中&#xff0c;提到了etcd kv存储backend是基于boltdb实现的&#xff0c;其在boltdb的基础上封装了读写事务&#xff0c;通过内存缓存批量将事务刷盘&#xff0c;提升整体的写入性能。botldb是etcd的真正的底层存储。本…...

基于MATLAB开发AUTOSAR软件应用层模块-part21.SR interface通信介绍(包括isupdated判断通信)

这篇文章我们介绍最后一种interface,即Sender-Receiver Interface,这种通信方式是autosar架构中最常用的的通信方式,即一个SWC发送数据,另一个SWC接收数据,实现数据交互。下边我们介绍下这篇文章主要介绍的内容: 目录如下: 如何配置SR interface,实现SR 通信介绍含有…...

Kotlin新手教程八(泛型)

一、泛型 1.泛型类的创建与实例化 kotlin中泛型类的创建与实例化与Java中相似&#xff1a; class A<T>(t:T){var valuet }fun main() {var a:A<Int> A<Int>(11) }Kotlin中存在类型推断&#xff0c;所以创建实例可以写成&#xff1a; var aA(11)2.泛型约束…...

性能测试知多少?怎样开展性能测试

看到好多新手&#xff0c;在性能需求模糊的情况下&#xff0c;随便找一个性能测试工具&#xff0c;然后就开始进行性能测试了&#xff0c;在这种情况下得到的性能测试结果很难体现系统真实的能力&#xff0c;或者可能与系统真实的性能相距甚远。 与功能测试相比&#xff0c;性能…...