前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言之延迟调用函数defer

Go语言之延迟调用函数defer

作者头像
灰子学技术
发布2023-10-30 15:17:41
1190
发布2023-10-30 15:17:41
举报
文章被收录于专栏:灰子学技术
写在前面的话:

在接触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被声明时,其参数就会被实时解析。

代码语言:javascript
复制
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:

代码语言:javascript
复制
Main i value: 2
defer i value: 1

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

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

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

代码语言:javascript
复制
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:

代码语言:javascript
复制
Main i value: 4
defer i value: 3
defer i value: 2
defer i value: 1

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

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

代码语言:javascript
复制
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:

代码语言:javascript
复制
Main  i value: 1
Main0 i value: 2
Main1 i value: 1
Main2 i value: 2
三.defer的实现原理

1)defer 的数据结构

代码语言:javascript
复制
type _defer struct{ 
  spuintptr//函数栈指针 
  pcuintptr//程序计数器
  fn*funcval//函数地址
  link*_defer//指向自身结构的指针,用于链接多个defer
}

图片发自简书App

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

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

2)defer的创建与执行

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

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

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

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

https://studygolang.com/articles/16067

灰子作于二零一九年三月十九日。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-07-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 灰子学技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面的话:
  • 一.为什么我们需要defer
  • 二.defer的使用规则
  • output:
  • OutPut:
  • Output:
    • 三.defer的实现原理
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档