资讯

精准传达 • 有效沟通

从品牌网站建设到网络营销策划,从策略到执行的一站式服务

go语言defer分析,golang defer原理

3.6 Go语言函数的延迟调用(Deferred Code)

在以下这段代码中,我们操作一个文件,无论成功与否都需要关闭文件句柄。这里在三处不同的位置都调用了file.Close()方法,代码显得非常冗余。

八宿网站制作公司哪家好,找创新互联公司!从网页设计、网站建设、微信开发、APP开发、成都响应式网站建设公司等网站项目制作,到程序开发,运营维护。创新互联公司于2013年开始到现在10年的时间,我们拥有了丰富的建站经验和运维经验,来保证我们的工作的顺利进行。专注于网站建设就选创新互联公司

我们利用延迟调用来优化代码。定义后的defer代码,会在return之前返回,让代码显得更加紧凑,且可读性变强,对上面的代码改造如下:

我们通过这个示例来看一下延迟调用与正常代码之间的执行顺序

先简单分析一下代码逻辑:

从输出中,我们可以观察到如下现象:

从这个实例中,我们很明显观察到,defer语句是在return之前执行

如果一个函数内定义了多个defer,则调用顺序为LIFO(后进先出)方式执行。

仍然是相同的例子,但是在TestDefer中我们定义了三个defer输出,根据LIFO原则,输出的顺序是3rd-2nd-1st,根据最后的结果,也是逆向向上执行defer输出。

就在整理这篇笔记的时候,发现了自己的认知误区,主要是本节实例三中发现的,先来看一下英文的描述:

对于上面的这段话的理解:

下面是代码执行输出,我们来一起分析一下:

虽然在a()函数内,显示的返回了10,但是main函数中得到的结果是defer函数自增后的结果,我们来分析一下代码:

在这篇文章的上一版,我曾经尝试用指针取解释defer修改返回值的类型,但是感觉不够透彻,也让阅读者非常困惑,索性参考了一下go官方blog中的一篇文章,在此基础上进行了扩展。如需要阅读原文,可以参考下面的文章。

go语言中关于defer的注意事项

举个例子,如下

答案

解析:

defer函数在处理Panic() 和Recover()时的应用

panic 函数是内置的go函数,它 终止 go程序的当前流程并开始 panicking , recover 函数也是内置的一个go函数,允许你收回处理那些使用了 panic 函数的 goroutine 的控制权

来个案例

1.14版本defer性能大幅度提升,内部实现了开放编码优化

GO中的defer会在当前函数返回前执行传入的函数,常用于关闭文件描述符,关闭链接及解锁等操作。

Go语言中使用defer时会遇到两个常见问题:

接下来我们来详细处理这两个问题。

官方有段对defer的解释:

这里我们先来一道经典的面试题

你觉得这个会打印什么?

输出结果:

这里是遵循先入后出的原则,同时保留当前变量的值。

把这道题简化一下:

输出结果

上述代码输出似乎不符合预期,这个现象出现的原因是什么呢?经过分析,我们发现调用defer关键字会立即拷贝函数中引用的外部参数,所以fmt.Println(i)的这个i是在调用defer的时候就已经赋值了,所以会直接打印1。

想要解决这个问题也很简单,只需要向defer关键字传入匿名函数

这里把一些垃圾回收使用的字段忽略了。

中间代码生成阶段cmd/compile/internal/gc/ssa.go会处理程序中的defer,该函数会根据条件不同,使用三种机制来处理该关键字

开放编码、堆分配和栈分配是defer关键字的三种方法,而Go1.14加入的开放编码,使得关键字开销可以忽略不计。

call方法会为所有函数和方法调用生成中间代码,工作内容:

defer关键字在运行时会调用deferproc,这个函数实现在src/runtime/panic.go里,接受两个参数:参数的大小和闭包所在的地址。

编译器不仅将defer关键字转成deferproc函数,还会通过以下三种方式为所有调用defer的函数末尾插入deferreturn的函数调用

1、在cmd/compile/internal/gc/walk.go的walkstmt函数中,在遇到ODEFFER节点时会执行Curfn.Func.SetHasDefer(true),设置当前函数的hasdefer属性

2、在ssa.go的buildssa会执行s.hasdefer = fn.Func.HasDefer()更新hasdefer

3、在exit中会根据hasdefer在函数返回前插入deferreturn的函数调用

runtime.deferproc为defer创建了一个runtime._defer结构体、设置它的函数指针fn、程序计数器pc和栈指针sp并将相关参数拷贝到相邻的内存空间中

最后调用的return0是唯一一个不会触发延迟调用的函数,可以避免deferreturn的递归调用。

newdefer的分配方式是从pool缓存池中获取:

这三种方式取到的结构体_defer,都会被添加到链表的队头,这也是为什么defer按照后进先出的顺序执行。

deferreturn就是从链表的队头取出并调用jmpdefer传入需要执行的函数和参数。

该函数只有在所有延迟函数都执行后才会返回。

如果我们能够将部分结构体分配到栈上就可以节约内存分配带来的额外开销。

在call函数中有在栈上分配

在运行期间deferprocStack只需要设置一些未在编译期间初始化的字段,就可以将栈上的_defer追加到函数的链表上。

除了分配的位置和堆的不同,其他的大致相同。

Go语言在1.14中通过开放编码实现defer关键字,使用代码内联优化defer关键的额外开销并引入函数数据funcdata管理panic的调用,该优化可以将 defer 的调用开销从 1.13 版本的 ~35ns 降低至 ~6ns 左右。

然而开放编码作为一种优化 defer 关键字的方法,它不是在所有的场景下都会开启的,开放编码只会在满足以下的条件时启用:

如果函数中defer关键字的数量多于8个或者defer处于循环中,那么就会禁用开放编码优化。

可以看到这里,判断编译参数不用-N,返回语句的数量和defer数量的乘积小于15,会启用开放编码优化。

延迟比特deferBitsTemp和延迟记录是使用开放编码实现defer的两个最重要的结构,一旦使用开放编码,buildssa会在栈上初始化大小为8个比特的deferBits

延迟比特中的每一个比特位都表示该位对应的defer关键字是否需要被执行。延迟比特的作用就是标记哪些defer关键字在函数中被执行,这样就能在函数返回时根据对应的deferBits确定要执行的函数。

而deferBits的大小为8比特,所以该优化的条件就是defer的数量小于8.

而执行延迟调用的时候仍在deferreturn

这里做了特殊的优化,在runOpenDeferFrame执行开放编码延迟函数

1、从结构体_defer读取deferBits,执行函数等信息

2、在循环中依次读取执行函数的地址和参数信息,并通过deferBits判断是否要执行

3、调用reflectcallSave执行函数

1、新加入的defer放入队头,执行defer时是从队头取函数调用,所以是后进先出

2、通过判断defer关键字、return数量来判断是否开启开放编码优化

3、调用deferproc函数创建新的延迟调用函数时,会立即拷贝函数的参数,函数的参数不会等到真正执行时计算


名称栏目:go语言defer分析,golang defer原理
当前地址:http://www.cdkjz.cn/article/hchpgo.html
多年建站经验

多一份参考,总有益处

联系快上网,免费获得专属《策划方案》及报价

咨询相关问题或预约面谈,可以通过以下方式与我们联系

大客户专线   成都:13518219792   座机:028-86922220