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

news2024/12/24 2:06:18

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 main

import "fmt"

var ErrTooLarge = 2

func 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 main

import "fmt"

func foo(a, b int) (x, y int) {
	defer func() {
		x = x * 5
		println("3_x=", x)
		y = y * 10
		println("4_y=", y)
	}()
	x = a + 5
	println("1_x=", x)
	y = b + 6
	println("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 main

import "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.coloradmin.cn/o/1000177.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

IAM、EIAM、CIAM、RAM、IDaaS 都是什么?

后端程序员在做 ToB 产品或者后台系统时&#xff0c;都不可避免的会遇到账号系统、登录系统、权限系统、日志系统等这些核心功能。这些功能一般都是以 SSO 系统、RBAC 权限管理系统等方式命名&#xff0c;但这些系统合起来有一个专有名词&#xff1a;IAM。 IAM IAM 是 Identi…

视频直播点播平台EasyDSS流媒体服务器按时间调用录像,提示数据查询错误是什么原因?

EasyDSS能实现视频流媒体的上传、转码、存储、录像、推拉流、直播、点播等功能&#xff0c;具备超低延迟、超高画质、超大并发访问量等特点&#xff0c;可应用在多样化的场景中&#xff0c;如&#xff1a;在线课堂、教育直播、校园活动直播、企业培训、游戏直播等。为了便于用户…

【面试经典150 | 双指针】两数之和

文章目录 写在前面Tag题目来源题目解读解题思路方法一&#xff1a;暴力枚举方法二&#xff1a;哈希表方法三&#xff1a;二分法方法四&#xff1a;双指针 知识回顾写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢…

[NLP]LLM--使用LLama2进行离线推理

一 模型下载 二 模型推理 本文基于Chinese-LLaMA-Alpaca-2项目代码介绍&#xff0c;使用原生的llama2-hf 克隆好了Chinese-LLaMA-Alpaca-2 项目之后&#xff0c;基于GPU的部署非常简单。下载完成以后的模型参数(Hugging Face 格式)如下&#xff1a; 简单说明一下各个文件的作…

【精品】git commit 代码规范

规范 格式&#xff1a; type(scope) : subject type&#xff08;必须&#xff09; : commit 的类别&#xff0c;只允许使用下面几个标识&#xff1a; feat : 新功能fix : 修复bugdocs : 文档改变style : 代码格式改变refactor : 某个已有功能重构perf : 性能优化test : 增加测…

6.2.2 【MySQL】InnoDB中的索引方案

上边之所以称为一个简易的索引方案&#xff0c;是因为我们为了在根据主键值进行查找时使用二分法快速定位具体的目录项而假设所有目录项都可以在物理存储器上连续存储&#xff0c;但是这样做有几个问题&#xff1a; InnoDB 是使用页来作为管理存储空间的基本单位&#xff0c;也…

未来3-5年,前端低代码化,具体往哪个方向发展更好就业?

最近发现一个有趣的现象&#xff0c;好像是要紧追AIGC的速度一样&#xff0c;我的朋友圈也是越来越多人每天抒发关于“前端开发新方向”的见解。 且其中不仅是关于AI的讨论&#xff0c;还把不少资深人士把低代码也拉出来溜了一圈&#xff0c;不仅是开发人员&#xff0c;产品经理…

构建企业分支网络

构建企业分支网络 目录 1.1 项目背景 1.2 项目拓扑 1.3 项目需求 1.4 设备选型 1.5 技术选型 1.6 地址规划 1.6.1 交换设备地址规划表 1.6.2 路由设备地址规划表 1.6.3 ISP设备地址规划表 1.6.4 终端地址规划表 1.6.4.1 VLAN 规划 1.7 VLAN 规划 1.8 项目实施 1.…

git撤回 不小心 commit 进去的文件

我时候 我们可能讲一下不想提交的文件 不小心commit了进去 我们可以通过 git reset HEAD~来撤回刚才的添加记录

iNeuOS工业互联网操作系统V5,视图建模(WEB组态)升级

针对工业来讲&#xff0c;特殊是流程行业&#xff0c;视图建模&#xff08;Web组态&#xff09;是必不可少应用场景&#xff0c;因为有很多工序要直观的展示工艺流程图。 对于一个工厂&#xff0c;少则几十张工艺流程图&#xff0c;多则上百张工艺流程图&#xff0c;还得支持灵…

被逼出来的自主可控,从华为自研看国产 IDE 的未来和商业模式

华为的自研 IDE 之路 我所在的部门“华为云 PaaS 服务产品部”在软件开发工具领域肩负着两大使命&#xff1a;一是为华为内部各产业开发者提供软件开发工具&#xff0c;提升开发效率&#xff1b;二是以华为云为承载平台&#xff0c;将华为内部优秀的软件工程工具和研发实践服务…

优维产品最佳实践:流水线的编排

前言&#xff1a;在前面的内容中&#xff0c;我们已经深入探讨了流水线的设计思路以及“一次构建多次部署”的核心概念。现在&#xff0c;让我们将这些理论知识付诸实践&#xff0c;在 EasyOps 平台上开始编排流水线。 本期优维EasyOps产品使用最佳实践&#xff0c;我们将为您…

视频监控平台EasyCVR分组批量绑定/取消通道功能的后端代码设计逻辑介绍

视频监控平台/视频存储/视频分析平台EasyCVR基于云边端一体化管理&#xff0c;可支持视频实时监控、云端录像、云存储、磁盘阵列存储、回放与检索、智能告警、平台级联等功能。安防监控平台在线下场景中应用广泛&#xff0c;包括智慧工地、智慧工厂、智慧校园、智慧社区等等。 …

真实软件测试案例测试报告编写规划

一、什么是测试报告&#xff1f; 测试报告是指把测试的过程和结果写成文档&#xff0c;对发现的问题和缺陷进行分析&#xff0c;为纠正软件存在的质量问题提供依据&#xff0c;同时为软件验收和交付打下基础。 二、测试执行和结束的准则 1、测试执行的结束的原因 1&#xff…

正规好用的电脑端抽奖软件有哪些?

这几个软件都是本人反复用过、反复比较的&#xff0c;且都超过5年。 1. 518抽奖软件 518抽奖软件&#xff0c;518我要发&#xff0c;超好用的年会抽奖软件&#xff0c;简约设计风格。 包含文字号码抽奖、照片抽奖两种模式&#xff0c;支持姓名抽奖、号码抽奖、数字抽奖、照片抽…

珠宝行业如何进行有效的软文推广?媒介盒子告诉你

在当今时代&#xff0c;珠宝不仅是一种饰品&#xff0c;更是一种身份的象征&#xff0c;因此珠宝行业的竞争越来越激烈&#xff0c;为了让自己的品牌脱颖而出&#xff0c;珠宝企业需要进行有效的推广&#xff0c;而软文推广就是一种非常有效的方式。也有很多珠宝品牌来找盒子进…

svg 知识点总结

1. 引用 svg&#xff0c;直接用 img 标签 <img src"帐篷.svg" alt"露营">2. 画 svg 各种图形。 矩形 rect圆角矩形 rect圆圈 circle椭圆 ellipse线段 line折线 polyline多边形 polygon路径 path <svg width"200" height"250&qu…

C++初阶--类和对象(中)

目录 类的6个默认成员函数构造函数使用方法 析构函数使用方法 拷贝构造函数使用方法 赋值运算符重载赋值运算符重载 const成员 上篇末尾我们讲到了关于c实现栈相较于c语言在传递参数时的一些优化&#xff0c;但实际上&#xff0c;c在 初始化 清理 赋值 拷贝等方面也做了很大程…

照片太大怎么缩小kb?

照片太大怎么缩小kb&#xff1f;在日常使用电脑或手机时&#xff0c;我们经常会遇到照片过大而无法在聊天工具中传输的情况。这种情况非常常见且正常。当我们拍摄或保存的照片文件体积较大时&#xff0c;不仅会给传输带来困扰&#xff0c;还会占据宝贵的手机和电脑内存空间&…

Beyond Compare:文件夹和文件对比专家

在处理文件和文件夹时&#xff0c;我们有时需要比较两个文件或文件夹是否一致。在这个过程中&#xff0c;Beyond Compare 这款专业的文件夹和文件对比工具成为了我们的得力助手。下面&#xff0c;让我们一起来了解这款工具的基本使用说明。 一、Beyond Compare的下载与安装 首…