Go语言之Defer篇

ZhDavis · · 995 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

转发自自己的公众号(灰子学技术)。

原文链接:https://mp.weixin.qq.com/s/5V0eeDpbe937j-6FtWpRUQ

写在前面的话:

在接触defer之后,觉得Go的这一特性很好,有点类似于C++的析构函数,不过他们却有很大的不同。主要的区别点是defer实现在函数里面,作用域也是在函数里面,当函数的return语句被调用之后,才会调用到这个defer声明的函数。而析构函数实现在类里面,作用域是在类内部,在该类的实例被销毁的时候,就会被调用到。

在谈论defer之前,笔者问了自己三个问题:

 

为什么我们需要defer?

 

如何才能更好的使用它?

 

defer是如何实现的?

基于上面的三个问题,笔者做了简单的整理。

一.为什么我们需要defer

我们在写程序的时候,往往会碰到下面的两种情况。

第一种释放资源,当我们在创建一个资源的时候,往往需要释放资源,但是因为逻辑分支太多的缘故,我们要在每一个异常分支里面去实现释放资源的 操作。这样以来的话,就存在两个问题,第一,我们需要散弹式修改,释放资源的地方很多,每个都要填写上面,代码不容易维护。第二,异常分支太多的话,很容易漏掉,或者提前return了,进而导致资源没有释放掉,这样会产生代码漏洞。

第二种处理异常,代码实现里面,有一些异常是可以从逻辑代码里面控制的,有一些却未必容易控制,特别是一些很难捕捉到到异常,这种主要来源于操作系统内核或者硬件提示的异常信息。

1.C++里面这两种情况,都有对应的处理方法,第一种采用析构函数去释放这些资源,第二种情况采用try-catch的方式去捕获和处理这些异常(备注:这部分内容会专门整理一篇文章介绍)。

2.到了Go之后,我发现C++的这两种实现方式都不存在了,那怎么办呢?于是defer产生了,这种在普通函数的return之后会调用的延迟调用函数,该发挥作用了。

二.defer的使用规则

defer函数调用时间,发生在该函数的return之后,主要用三种使用规则,说是三种规则,其实更像是三种注意事项。

1)当defer被声明时,其参数就会被实时解析。

package main

 

import (

 

"fmt"

 

)

 

func main() {

 

var i int = 1

 

// i的值在defer第一次走到的位置就被确认下来了

 

defer fmt.Println("defer i value:", i) 

 

i++

 

fmt.Println("Main i value:", i)

 

}

output:

Main i value: 2

 

defer i value: 1

备注:对于指针来说,这个参数是地址,指针指向的数据还是有可能会被更改的。

2)当一个函数中有多个defer函数时,它们的执行顺序是先进后出。

这种处理场景,一般是有几个资源,而这些资源之间是有依赖关系的。

package main

 

import (

 

"fmt"

 

)

 

func main() {

 

var i int = 1

 



 

defer fmt.Println("defer i value:", i) // 3rd called

 

i++

 

defer fmt.Println("defer i value:", i) // 2nd called

 

i++

 

defer fmt.Println("defer i value:", i) // 1st called

 

i++

 

fmt.Println("Main i value:", i)

 

}

OutPut:

Main i value: 4

 

defer i value: 3

 

defer i value: 2

 

defer i value: 1

3)defer可以读取有名返回值。

关于这一个规则,笔者觉得这是defer的一个副作用,毕竟返回值在return之后,是不希望被改掉的。不过也有好处,就是一旦希望对函数的返回值做一些特殊操作的时候,例如希望将返回值占内存很大的内容写到文件里或者内存里。

package main

 

import (

 

"fmt"

 

)

 

func deferFunc() (i int ) {

 

return 1 // 返回值1

 

}

 

func deferFunc0() (i int ) {

 

defer func() { i++ }() // 会更改i的值,但是没有办法影响返回值

 

return 1  // 返回值 直接将1这种数字写回了栈中,并直接返回了

 

}

 

func deferFunc1() (i int ) {

 

defer func() { i++ }()// step2: 会继续操作i++

 

return i // step1: 返回值会复制给一个临时变量,再返回出去

 

}

 

func main() {

 

i := 0

 

i = deferFunc()

 

fmt.Println("Main  i value:", i)

 

i = deferFunc0()

 

fmt.Println("Main0 i value:", i)

 

i = deferFunc1()

 

fmt.Println("Main1 i value:", i)

 

i++

 

fmt.Println("Main2 i value:", i)

 

}

Output:

Main  i value: 1

 

Main0 i value: 2

 

Main1 i value: 1

 

Main2 i value: 2

三.defer的实现原理

1)defer 的数据结构

type_deferstruct{

 

spuintptr//函数栈指针

 

pcuintptr//程序计数器

 

fn*funcval//函数地址

 

link*_defer//指向自身结构的指针,用于链接多个defer

 

}

 

每次声明一个defer函数都会从链表头部开始插入。

函数返回前执行defer是从链表首部一次取出执行。

2)defer的创建与执行

deferproc():在声明defer处调用,将其defer函数存入goroutine的链表中。

deferreturn():在ret指令前调用,将defer从对应的链表中取出并执行。

过程如下:在编译阶段声明defer处插入函数deferproc() ,在函数return前插入函数deferreturn()。

参考资料:Go语言程序设计

https://studygolang.com/articles/16067

欢迎关注,订阅,评论,共同学习,共同进步!

灰子学技术:

 

 

 


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

995 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传