Go_defer详解

defer

1. 前言

defer语句用于延迟函数的调用,每次defer都会把一个函数压入栈中,函数返回前再把延迟的函数取出并执行。

为了方便描述,我们把创建defer的函数称为主函数,defer语句后面的函数称为延迟函数。

延迟函数可能有输入参数,这些参数可能来源于定义defer的函数,延迟函数也可能引用主函数用于返回的变量,也就是说延迟函数可能会影响主函数的一些行为,这些场景下,如果不了解defer的规则很容易出错。

其实官方说明的defer的三个原则很清楚,本节试图汇总defer的使用场景并做简单说明。

2. 热身

按照惯例,我们看几个有意思的题目,用于检验对defer的了解程度。

2.1 题目一

下面函数输出结果是什么?

func deferFuncParameter() {
    var aInt = 1

    defer fmt.Println(aInt)

    aInt = 2
    return
}

题目说明:
函数deferFuncParameter()定义一个整型变量并初始化为1,然后使用defer语句打印出变量值,最后修改变量值为2.

参考答案:
输出1。延迟函数fmt.Println(aInt)的参数在defer语句出现时就已经确定了,所以无论后面如何修改aInt变量都不会影响延迟函数。

2.2 题目二

下面程序输出什么?

package main

import "fmt"

func printArray(array *[3]int) {
    for i := range array {
        fmt.Println(array[i])
    }
}

func deferFuncParameter() {
    var aArray = [3]int{1, 2, 3}

    defer printArray(&aArray)

    aArray[0] = 10
    return
}

func main() {
    deferFuncParameter()
}

函数说明:
函数deferFuncParameter()定义一个数组,通过defer延迟函数printArray()的调用,最后修改数组第一个元素。printArray()函数接受数组的指针并把数组全部打印出来。

参考答案:
输出10、2、3三个值。延迟函数printArray()的参数在defer语句出现时就已经确定了,即数组的地址,由于延迟函数执行时机是在return语句之前,所以对数组的最终修改值会被打印出来。

2.3 题目三

下面函数输出什么?

func deferFuncReturn() (result int) {
    i := 1

    defer func() {
       result++
    }()

    return i
}

函数说明:
函数拥有一个具名返回值result,函数内部声明一个变量i,defer指定一个延迟函数,最后返回变量i。延迟函数中递增result。

参考答案:
函数输出2。函数的return语句并不是原子的,实际执行分为设置返回值–>ret,defer语句实际执行在返回前,即拥有defer的函数返回过程是:设置返回值–>执行defer–>ret。所以return语句先把result设置为i的值,即1,defer语句中又把result递增1,所以最终返回2。

3. defer规则

Golang官方博客里总结了defer的行为规则,只有三条,我们围绕这三条进行说明。

3.1 规则一:延迟函数的参数在defer语句出现时就已经确定下来了

官方给出一个例子,如下所示:

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

defer语句中的fmt.Println()参数i值在defer出现时就已经确定下来,实际上是拷贝了一份。后面对变量i的修改不会影响fmt.Println()函数的执行,仍然打印”0”。

注意:对于指针类型参数,规则仍然适用,只不过延迟函数的参数是一个地址值,这种情况下,defer后面的语句对变量的修改可能会影响延迟函数。

3.2 规则二:延迟函数执行按后进先出顺序执行,即先出现的defer最后执行

这个规则很好理解,定义defer类似于入栈操作,执行defer类似于出栈操作。

设计defer的初衷是简化函数返回时资源清理的动作,资源往往有依赖顺序,比如先申请A资源,再根据A资源申请B资源,根据B资源申请C资源,即申请顺序是:A–>B–>C,释放时往往又要反向进行。这就是把defer设计成LIFO的原因。

每申请到一个用完需要释放的资源时,立即定义一个defer来释放资源是个很好的习惯。

package main

import "fmt"
func deferList() {
	defer fmt.Println("1")
	defer fmt.Println("2")
	defer fmt.Println("3")
}

func main() {
	deferList()
}
输出结果
3
2
1

3.3 规则三:延迟函数可能操作主函数的具名返回值

定义defer的函数,即主函数可能有返回值,返回值有没有名字没有关系,defer所作用的函数,即延迟函数可能会影响到返回值。

若要理解延迟函数是如何影响主函数返回值的,只要明白函数是如何返回的就足够了。

3.3.1 函数返回过程

有一个事实必须要了解,关键字return不是一个原子操作,实际上return只代理汇编指令ret,即将跳转程序执行。比如语句return i,实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的。

举个实际的例子进行说明这个过程:

func deferFuncReturn() (result int) {
    i := 1

    defer func() {
       result++
    }()

    return i
}

该函数的return语句可以拆分成下面两行:

result = i
return

而延迟函数的执行正是在return之前,即加入defer后的执行过程如下:

result = i
result++
return

所以上面函数实际返回i++值。

关于主函数有不同的返回方式,但返回机制就如上机介绍所说,只要把return语句拆开都可以很好的理解,下面分别举例说明

3.3.2 主函数拥有匿名返回值,返回字面值

一个主函数拥有一个匿名的返回值,返回时使用字面值,比如返回”1”、”2”、”Hello”这样的值,这种情况下defer语句是无法操作返回值的。

一个返回字面值的函数,如下所示:

func foo() int {
    var i int

    defer func() {
        i++
    }()

    return 1
}

上面的return语句,直接把1写入栈中作为返回值,延迟函数无法操作该返回值,所以就无法影响返回值。

3.3.3 主函数拥有匿名返回值,返回变量

一个主函数拥有一个匿名的返回值,返回使用本地或全局变量,这种情况下defer语句可以引用到返回值,但不会改变返回值。

一个返回本地变量的函数,如下所示:

func foo() int {
    var i int

    defer func() {
        i++
    }()

    return i
}

上面的函数,返回一个局部变量,同时defer函数也会操作这个局部变量。对于匿名返回值来说,可以假定仍然有一个变量存储返回值,假定返回值变量为”anony”,上面的返回语句可以拆分成以下过程:

anony = i
i++
return

由于i是整型,会将值拷贝给anony,所以defer语句中修改i值,对函数返回值不造成影响。

3.3.4 主函数拥有具名返回值

主函声明语句中带名字的返回值,会被初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果defer语句操作该返回值,可能会改变返回结果。

一个影响函返回值的例子:

func foo() (ret int) {
    defer func() {
        ret++
    }()

    return 0
}

上面的函数拆解出来,如下所示:

ret = 0
ret++
return

函数真正返回前,在defer中对返回值做了+1操作,所以函数最终返回1。

4. defer实现原理

本节我们尝试了解一些defer的实现机制。

4.1 defer数据结构

源码包src/src/runtime/runtime2.go:_defer定义了defer的数据结构:

type _defer struct {
    sp      uintptr   //函数栈指针
    pc      uintptr   //程序计数器
    fn      *funcval  //函数地址
    link    *_defer   //指向自身结构的指针,用于链接多个defer
}

我们知道defer后面一定要接一个函数的,所以defer的数据结构跟一般函数类似,也有栈地址、程序计数器、函数地址等等。

与函数不同的一点是它含有一个指针,可用于指向另一个defer,每个goroutine数据结构中实际上也有一个defer指针,该指针指向一个defer的单链表,每次声明一个defer时就将defer插入到单链表表头,每次执行defer时就从单链表表头取出一个defer执行。

下图展示多个defer被链接的过程:

null

从上图可以看到,新声明的defer总是添加到链表头部。

函数返回前执行defer则是从链表首部依次取出执行,不再赘述。

一个goroutine可能连续调用多个函数,defer添加过程跟上述流程一致,进入函数时添加defer,离开函数时取出defer,所以即便调用多个函数,也总是能保证defer是按LIFO方式执行的。

4.2 defer的创建和执行

源码包src/runtime/panic.go定义了两个方法分别用于创建defer和执行defer。

  • deferproc(): 在声明defer处调用,其将defer函数存入goroutine的链表中;
  • deferreturn():在return指令,准确的讲是在ret指令前调用,其将defer从goroutine链表中取出并执行。

可以简单这么理解,在编译阶段,声明defer处插入了函数deferproc(),在函数return前插入了函数deferreturn()。

5. 总结

  • defer定义的延迟函数参数在defer语句出现时就已经确定下来了
  • defer定义顺序与实际执行顺序相反
  • return不是原子操作,执行过程是: 保存返回值(若有)–>执行defer(若有)–>执行ret跳转
    声明defer处调用,其将defer函数存入goroutine的链表中;
  • deferreturn():在return指令,准确的讲是在ret指令前调用,其将defer从goroutine链表中取出并执行。

可以简单这么理解,在编译阶段,声明defer处插入了函数deferproc(),在函数return前插入了函数deferreturn()。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/269421.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

零基础入门网络安全必看的5本书籍(附PDF)

书中自有黄金屋,书中自有颜如玉。很多人学习一门技术都会看大量的书籍,经常也有朋友询问:零基础刚入门,应该看哪些书?应该怎么学?等等问题。今天就整理了5本零基础入门网络安全必看书籍,希望能帮…

WebGL开发地理和地球科学应用

使用WebGL开发地理和地球科学应用可以为学生提供交互式、沉浸式的学习体验,帮助他们理解地球表面的地理特征、地球科学原理以及环境变化。以下是开发地理和地球科学应用的一般步骤,希望对大家有所帮助。北京木奇移动技术有限公司,专业的软件外…

微信小程序~如何设置页面的背景色

微信小程序~如何设置页面的背景色 众所周知,微信小程序每个页面由.json,.scss,.ts,.wxml这四个文件组成。 有的小伙伴会发现,需要给页面加背景色的时候,只需在此页面的.scss文件中写个page{background-colo…

Java设计模式-桥接模式

目录 一、手机操作问题 二、传统方法 三、基本介绍 四、原理类图 五、使用桥接模式解决手机问题 一、手机操作问题 现在对不同手机类型的不同品牌实现操作编程( 比如 : 开机、关机、上网,打电话等) , 如图: 二、传统方法 传统方案解决手机操作问题分…

2024如果创业适合干什么,普通人如何创业

五行学说是中国古代哲学中的一部分,将世界万物归纳为五种元素,分别是金、木、水、火和土。每个年份的五行组合不同,代表着特定的能量和属性。2024年什么生意好做。 一,物联网科技行业:庚子年的金属性与现代物联网科技的…

【JUnit技术专题】「入门到精通系列」手把手+零基础带你玩转单元测试,让你的代码更加“强壮”(夯实功底篇)

手把手零基础带你玩转单元测试,让你的代码更加“强壮” 前言介绍JUnit是什么?JUnit和xUnit之间的关系 JUnit的基本概念JUnit的特点什么是一个单元测试用例 JUnit的用法JUnit的最佳实践案例分析创建一个类创建 Test Case 类创建 Test Runner 类 JUnit总体…

第11章 GUI Page436 使用缓冲DC, wxBufferedPaintDC

所谓“缓冲DC”,是指将所有图元都先划到一个人眼看不到的“设备上下文”之上,最后再一次性复制到真正的屏幕DC之上,这样我们就看不到中间画的过程了,也就不会感到闪烁了。 注意,这时不能解除ScrolledWindow1的背景擦除…

千帆起航:探索百度智能云千帆AppBuilder在AI原生应用开发中的革新之路

千帆起航:探索百度千帆AppBuilder在AI原生应用开发中的革新之路 1.揭开帷幕,大模型第二次战役 自从 ChatGPT 横空出世后,一石激起千层浪,人工智能也正在从感知理解走向生成创造,这是一个关键里程碑。生成式大模型完成…

[x86汇编语言]从实模式到保护模式第二版

下载汇编器:https://www.nasm.us/pub/nasm/releasebuilds/2.16.02rc6/win64/ mov ax, 0x3f add bx,ax add cx,ax 编译: C:\Users\HP>cd D:\BaiduNetdiskDownload\01b站\lizhong\myasm C:\Users\HP>D: D:\BaiduNetdiskDownload\01b站\lizhong…

H.264宏块(Macroblock)概念(运动估计、变换编码、环路滤波)

参考文章:音视频高手课系列5-h264编码基础(宏块原理) 参考文章:切片slice与宏块,运动矢量 文章目录 使用videoEye分析视频宏块示例H.264宏块概念1. 宏块的定义2. 运动估计3. 变换编码4. 环路滤波5. 注意:宏块的概念既适用于帧内编…

智能优化算法应用:基于侏儒猫鼬算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用:基于侏儒猫鼬算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用:基于侏儒猫鼬算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.侏儒猫鼬算法4.实验参数设定5.算法结果6.…

虚拟机(centos)磁盘空间不够

磁盘空间不够 1.关机 2.打开设置-->硬盘 --> 扩展 3.搞定一半 4.开机进入终端 4.1 查看 lsblk 4.2 加起来 growpart /dev/sda 3 注1: 如果 growpart 提示没有的话需要安装一下 yum -y install cloud-utils-growpart 注2:3前面有个空格 4.3 再次…

java八股 redis

Redis篇-01-redis开篇_哔哩哔哩_bilibili 1.缓存穿透 2.缓存击穿 逻辑过期里的互斥锁是为了保证只有一个线程去缓存重建 3.缓存雪崩 4.双写一致性 4.1要求一致性(延迟双删/互斥锁) 延迟双删无法保证强一致性 那么前两步删缓和更新数据库哪个先呢&#xf…

实时数据获取:抖音API在电商中的应用与影响

在电商行业高速发展的今天,数据已经成为企业决策和创新的重要驱动力。抖音作为全球最大的短视频平台之一,其根据关键词取商品列表API为电商行业带来了前所未有的机遇和挑战。本文将深入探讨该API在电商行业中的关键作用,以及如何实现实时数据…

day48算法训练|动态规划part09

198.打家劫舍 1. dp数组(dp table)以及下标的含义 dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。 2.递推公式 决定dp[i]的因素就是第i房间偷还是不偷。 如果偷第i房间,那么…

微信小程序获取用户手机号码教程(前端+后端)

1.背景介绍 在开发一款微信小程序时,需要用户进行微信登录,获取用户的手机号码来作为用户的唯一标识(userId),于是探索获取用户手机号码的方式; (当然,通过wx.login来获取code&…

JavaOOP篇----第十七篇

系列文章目录 文章目录 系列文章目录前言一、怎么在JDBC内调用一个存储过程二、是否了解连接池,使用连接池有什么好处?三、你所了解的数据源技术有那些?使用数据源有什么好处?四、&和&&的区别五、静态内部类如何定义前言 前些天发现了一个巨牛的人工智能学习网…

❀My学习小记录之XML❀

目录 ❀My学习小记录之XML❀ 一、简介 二、发展历程 XML历史: 三、XML常见应用 四、语法格式及相关介绍 语法: 格式声明(指令): 元素: 标记(标签): 属性: 注…

【Docker容器精解篇 】深入探索Docker技术的概念与容器思想

🎬 鸽芷咕:个人主页 🔥 个人专栏: 《docker容器精解篇》《粉丝福利》 ⛺️生活的理想,就是为了理想的生活! 文章目录 前言一、Docker 的介绍1.1 Docker 的由来1.1.1 环境不一致1.1.2 隔离性1.1.3 弹性伸缩1.1.4 学习成本 1.2 Doc…

Python遥感影像深度学习指南(3)-卫星图像语义分割之用PyTorch创建一个简单的U-Net 模型

在上一篇文章中,介绍了如何在不使用 torchvision 模块的情况下,创建卫星图像的多通道数据集。现在,我们将继续创建一个简单的深度学习模型,用于卫星图像的语义分割。 1、介绍 下图来自 "卷积神经网络实现了从高分辨率无人机图像中高效、准确、精细地分割植物物种和群…