Go基础16-defer的运作机制及常见用法

defer的运作离不开函数,这至少有两层含义:

● 在Go中,只有在函数和方法内部才能使用defer;

● defer关键字后面只能接函数或方法,这些函数被称为deferred函数。defer将它们注册到其所在goroutine用于存放deferred函数的栈数据结构中,这些deferred函数将在执行defer的函数退出前被按后进先出(LIFO)的顺序调度执行,如下图:deferred函数的存储与调度执行

在这里插入图片描述
无论是执行到函数体尾部返回,还是在某个错误处理分支显式调用return返回,抑或出现panic,已经存储到deferred函数栈中的函数都会被调度执行。

因此,deferred函数是一个在任何情况下都可以为函数进行收尾工作的好场合。我们回到本条开头的例子,把收尾工作挪到deferred函数中,变更后的代码如下:

func writeToFile(fname string, data []byte, mu *sync.Mutex) error {mu.Lock()defer mu.Unlock()f, err := os.OpenFile(fname, os.O_RDWR, 0666)if err != nil {return err}defer f.Close()_, err = f.Seek(0, 2)if err != nil {return err}_, err = f.Write(data)if err != nil {return err}return f.Sync()
}

我们看到,defer的使用对函数writeToFile的实现逻辑的简化是显而易见的,资源释放函数的defer注册动作紧邻着资源申请成功的动作。这样成对出现的惯例极大降低了遗漏资源释放的可能性,开发人员再也不用小心翼翼地在每个错误处理分支中检查是否遗漏了某个资源的释放动作。同时,代码的简化又意味代码可读性的提高以及健壮性的增强。

defer的常见用法

除了释放资源这个最基本、最常见的用法之外,defer的运作机制决定了它还可以在其他一些场合发挥作用,这些用法在Go标准库中均有体现。

  1. 拦截panic

defer的运行机制决定了无论函数是执行到函数体末尾正常返回,还是在函数体中的某个错误处理分支显式调用return返回,抑或函数体内部出现panic,已经注册了的deferred函数都会被调度执行。

因此,defer的第二个重要用途就是拦截panic,并按需要对panic进行处理,可以尝试从panic中恢复(这也是Go语言中唯一的从panic中恢复的手段),也可以如下面标准库代码中这样触发一个新panic,但为新panic传一个新的error值:

package mainimport "fmt"var ErrTooLarge = 2func makeSlice(n int) []byte {// If the make fails, give a known error.defer func() {if recover() != nil {fmt.Printf("333", 333)panic(ErrTooLarge) // 触发一个新panic}}()return make([]byte, n)
}//下面的代码则通过deferred函数拦截panic并恢复了程序的运行:
// chapter4/sources/deferred_func_3.go
func bar() {fmt.Println("raise a panic")panic(-1)
}
func foo() {defer func() {if e := recover(); e != nil {fmt.Println("recovered from a panic")}}()bar()
}
func main() {foo()fmt.Println("main exit normally")
}

运行结果:

raise a panic
recovered from a panic
main exit normally

deferred函数在出现panic的情况下依旧能够被调度执行,这一特性让下面两个看似行为等价的函数在程序触发panic的时候得到不同的执行结果:

var mu sync.Mutex
func f() {mu.Lock()defer mu.Unlock()bizOperation()
}
func g() {mu.Lock()bizOperation()mu.Unlock()
}

当函数bizOperation抛出panic时,函数g无法释放mutex,而函数f则可以通过deferred函数释放mutex,让后续函数依旧可以申请mutex资源。

deferred函数虽然可以拦截绝大部分的panic,但无法拦截并恢复一些运行时之外的致命问题。比如下面代码中通过C代码“制造”的崩溃,deferred函数便无能为力:

package main
//#include <stdio.h>
//void crash() {
// int *q = NULL;
// (*q) = 15000;
// printf("%d\n", *q);
//}
import "C"
import (
"fmt"
)
func bar() {
C.crash()
}
func foo() {
defer func() {
if e := recover(); e != nil {
fmt.Println("recovered from a panic:", e)
}
}()
bar()
}
func main() {foo()fmt.Println("main exit normally")
}

执行这段代码我们就会看到,虽然有deferred函数拦截,但程序仍然崩溃了:

SIGILL: illegal instruction
PC=0x409a7f4 m=0 sigcode=1
goroutine 0 [idle]:
runtime: unknown pc 0x409a7f4
  1. 修改函数的具名返回值

下面是Go标准库中通过deferred函数访问函数具名返回值变量的两个例子:

func (s *ss) Token(skipSpace bool, f func(rune) bool) (tok []byte, err error) {
defer func() {
if e := recover(); e != nil {
if se, ok := e.(scanError); ok {err = se.err
} else {panic(e)
}
}
}()
...
}
// $GOROOT/SRC/net/ipsock_plan9.go
func dialPlan9(ctx context.Context, net string, laddr, raddr Addr) (fd *netFD, err error) {defer func() { fixErr(err) }()
...
}

我们也来写一个更直观的示例:

package mainimport "fmt"func foo(a, b int) (x, y int) {defer func() {x = x * 5println("3_x=", x)y = y * 10println("4_y=", y)}()x = a + 5println("1_x=", x)y = b + 6println("2_y=", y)return
}
func main() {x, y := foo(1, 2)fmt.Println("x=", x, "y=", y)
}

运行结果:

1_x= 6
2_y= 8
3_x= 30
4_y= 80
x= 30 y= 80

我们看到deferred函数在foo真正将执行权返回给main函数之前,将foo的两个返回值x和y分别放大了5倍和10倍。

输出调试信息

deferred函数被注册及调度执行的时间点使得它十分适合用来输出一些调试信息。比如,Go标准库中net包中的hostLookupOrder方法就使用deferred函数在特定日志级别下输出一些日志以便于程序调试和跟踪。

func (c *conf) hostLookupOrder(r *Resolver, hostname string) (ret hostLookupOrder) {
if c.dnsDebugLevel > 1 {
defer func() {
print("go package net: hostLookupOrder(", hostname, ") = ", ret.String(), "\n")
}()
}
...
}

更为典型的莫过于在出入函数时打印留痕日志(一般在调试日志级别下),这里摘录Go官方参考文档中的一个实现:

package mainimport "fmt"func trace(s string) string {fmt.Println("entering1:", s)return s
}
func un(s string) {fmt.Println("leaving4444:", s)
}
func a() {defer un(trace("defer33-------------a"))fmt.Println("in a")
}
func b() {defer un(trace("b"))fmt.Println("22in b")a()
}
func main() {b()
}

运行结果如下:

entering1: b
22in b
entering1: defer33-------------a
in a
leaving4444: defer33-------------a
leaving4444: b

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

时序分解 | MATLAB实现RIME-VMD霜冰优化算法优化VMD变分模态分解信号分量可视化

时序分解 | MATLAB实现RIME-VMD霜冰优化算法优化VMD变分模态分解信号分量可视化 目录 时序分解 | MATLAB实现RIME-VMD霜冰优化算法优化VMD变分模态分解信号分量可视化效果一览基本介绍程序设计参考资料 效果一览 基本介绍 RIME-VMD【23年新算法】霜冰优化算法优化VMD变分模态分…

面向对象技术

面向对象技术 考情分析面向对象基本概念面向对象分析面向对象的设计原则面向对象测试统一建模语言事务关系图 考情分析 设计模式在新版教材被删除了 考察偏向面向对象的基本概念和UML建模 但是设计模型在案例和论文题目中出现 面向对象基本概念 c b a 面向对象分析 记忆面向对…

分布式、锁、延时任务

1. redission redission 原理 Redis分布式锁-这一篇全了解(Redission实现分布式锁完美方案) 2.zk 2.1 指令 ls / / 下有哪些子节点 get /zookeeper 查看某个子节点内容 create /aa “test” delete /aa set /aa “test01” 2.2 创建节点 模式 默认创建永久 create -e …

Linux命令200例:write用于向特定用户或特定终端发送信息

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌。CSDN专家博主&#xff0c;阿里云社区专家博主&#xff0c;2023年6月csdn上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师&#xff0…

读高性能MySQL(第4版)笔记06_优化数据类型(上)

1. 良好的逻辑设计和物理设计是高性能的基石 1.1. 反范式的schema可以加速某些类型的查询&#xff0c;但同时可能减慢其他类型的查询 1.2. 添加计数器和汇总表是一个优化查询的好方法&#xff0c;但它们的维护成本可能很 1.3. 将修改schema作为一个常见事件来规划 2. 让事情…

Redis原理:IntSet

&#xff08;笔记总结自b站黑马程序员课程&#xff09; 一、结构 IntSet是Redis中set集合的一种实现方式&#xff0c;基于整数数组来实现&#xff0c;并且具备长度可变、有序等特征。 结构如下&#xff1a; typedef struct intset {uint32_t encoding; //编码方式uint32_t l…

二叉树的顺序结构以及堆的实现——【数据结构】

W...Y的主页 &#x1f60a; 代码仓库分享 &#x1f495; 上篇文章&#xff0c;我们认识了什么是树以及二叉树的基本内容、表示方法……接下来我们继续来深入二叉树&#xff0c;感受其中的魅力。 目录 二叉树的顺序结构 堆的概念及结构 堆的实现 堆的创建 堆的初始化与…

盲打键盘的正确指法指南

简介 很多打字初学者&#xff0c;并不了解打字的正确指法规范&#xff0c;很容易出现只用两根手指交替按压键盘的“二指禅”情况。虽然这样也能实现打字&#xff0c;但是效率极低。本文将简单介绍盲打键盘的正确指法&#xff0c;以便大家在后续的学习和工作中能够提高工作效率…

LINUX 用户和组操作

目录 一、用户和组的分类 1、用户分类 2、组的分类 3、用户和组的配置文件 二、用户管理 1、添加用户 2、修改用户信息 3、修改用户密码 4、用户间切换 5、删除用户账号 6、sudo命令提高普通用户权限 三、用户组管理 1、创建用户组 2、修改用户组的属性 3、添加…

智慧安防/视频分析云平台EasyCVR不显示告警图片该如何解决?

安防视频监控平台EasyCVR可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安防视频监控的能力&#xff0c;也…

选择排序——直接选择排序

直接选择排序&#xff1a;&#xff08;以重复选择的思想为基础进行排序&#xff09; 1、简述 顾名思义就是选出一个数&#xff0c;再去抉择放哪里去。 设记录R1&#xff0c;R2…&#xff0c;Rn&#xff0c;对i1&#xff0c;2&#xff0c;…&#xff0c;n-1&#xff0c;重复下…

【docker快速部署微服务若依管理系统(RuoYi-Cloud)】

工作原因&#xff0c;需要一个比较完整的开源项目测试本公司产品。偶然发现RuoYi-Cloud非常适合&#xff0c;它有足够多的中间件&#xff0c;而且官方提供docker安装&#xff0c;但我本人在安装过程中遇到了很多坑&#xff0c;在这里记录一下防止下次会再次遇到。 项目地址 ht…