Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >go 学习笔记之解读什么是defer延迟函数

go 学习笔记之解读什么是defer延迟函数

作者头像
雪之梦技术驿站
发布于 2019-10-21 09:49:24
发布于 2019-10-21 09:49:24
36500
代码可运行
举报
运行总次数:0
代码可运行

Go 语言中有个 defer 关键字,常用于实现延迟函数来保证关键代码的最终执行,常言道: "未雨绸缪方可有备无患".

延迟函数就是这么一种机制,无论程序是正常返回还是异常报错,只要存在延迟函数都能保证这部分关键逻辑最终执行,所以用来做些资源清理等操作再合适不过了.

出入成双有始有终

日常开发编程中,有些操作总是成双成对出现的,有开始就有结束,有打开就要关闭,还有一些连续依赖关系等等.

一般来说,我们需要控制结束语句,在合适的位置和时机控制结束语句,手动保证整个程序有始有终,不遗漏清理收尾操作.

最常见的拷贝文件操作大致流程如下:

  1. 打开源文件
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
srcFile, err := os.Open("fib.txt")
if err != nil {
	t.Error(err)
	return
}	
  1. 创建目标文件
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
dstFile, err := os.Create("fib.txt.bak")
if err != nil {
	t.Error(err)
	return
}
  1. 拷贝源文件到目标文件
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
io.Copy(dstFile, srcFile)
  1. 关闭目标文件
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
dstFile.Close()
srcFile.Close()
  1. 关闭源文件
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
srcFile.Close()

值得注意的是: 这种拷贝文件的操作需要特别注意操作顺序而且也不要忘记释放资源,比如先打开再关闭等等!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func TestCopyFileWithoutDefer(t *testing.T) {
	srcFile, err := os.Open("fib.txt")
	if err != nil {
		t.Error(err)
		return
	}

	dstFile, err := os.Create("fib.txt.bak")
	if err != nil {
		t.Error(err)
		return
	}

	io.Copy(dstFile, srcFile)

	dstFile.Close()
	srcFile.Close()
}

> 「雪之梦技术驿站」: 上述代码逻辑还是清晰简单的,可能不会忘记释放资源也能保证操作顺序,但是如果逻辑代码比较复杂的情况,这时候就有一定的实现难度了!

可能是为了简化类似代码的逻辑,Go 语言引入了 defer 关键字,创造了"延迟函数"的概念.

  • defer 的文件拷贝
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func TestCopyFileWithoutDefer(t *testing.T) {
	if srcFile, err := os.Open("fib.txt"); err != nil {
		t.Error(err)
		return
	} else {
		if dstFile,err := os.Create("fib.txt.bak");err != nil{
			t.Error(err)
			return
		}else{
			io.Copy(dstFile,srcFile)
	
			dstFile.Close()
			srcFile.Close()
		}
	}
}
  • defer 的文件拷贝
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func TestCopyFileWithDefer(t *testing.T) {
	if srcFile, err := os.Open("fib.txt"); err != nil {
		t.Error(err)
		return
	} else {
		defer srcFile.Close()

		if dstFile, err := os.Create("fib.txt.bak"); err != nil {
			t.Error(err)
			return
		} else {
			defer dstFile.Close()

			io.Copy(dstFile, srcFile)
		}
	}
}

上述示例代码简单展示了 defer 关键字的基本使用方式,显著的好处在于 Open/Close 是一对操作,不会因为写到最后而忘记 Close 操作,而且连续依赖时也能正常保证延迟时机.

简而言之,如果函数内部存在连续依赖关系,也就是说创建顺序是 A->B->C 而销毁顺序是 C->B->A.这时候使用 defer 关键字最合适不过.

懒人福音延迟函数

> 官方文档相关表述见 Defer statements

如果没有 defer 延迟函数前,普通函数正常运行:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func TestFuncWithoutDefer(t *testing.T) {
	// 「雪之梦技术驿站」: 正常顺序
	t.Log("「雪之梦技术驿站」: 正常顺序")

	// 1 2
	t.Log(1)
	t.Log(2)
}

当添加 defer 关键字实现延迟后,原来的 1 被推迟到 2 后面而不是之前的 1 2 顺序.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func TestFuncWithDefer(t *testing.T) {
	// 「雪之梦技术驿站」: 正常顺序执行完毕后才执行 defer 代码
	t.Log(" 「雪之梦技术驿站」: 正常顺序执行完毕后才执行 defer 代码")

	// 2 1
	defer t.Log(1)
	t.Log(2)
}

如果存在多个 defer 关键字,执行顺序可想而知,越往后的越先执行,这样才能保证按照依赖顺序依次释放资源.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func TestFuncWithMultipleDefer(t *testing.T) {
	// 「雪之梦技术驿站」: 猜测 defer 底层实现数据结构可能是栈,先进后出.
	t.Log(" 「雪之梦技术驿站」: 猜测 defer 底层实现数据结构可能是栈,先进后出.")

	// 3 2 1
	defer t.Log(1)
	defer t.Log(2)
	t.Log(3)
}

相信你已经明白了多个 defer 语句的执行顺序,那就测试一下吧!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func TestFuncWithMultipleDeferOrder(t *testing.T) {
	// 「雪之梦技术驿站」: defer 底层实现数据结构类似于栈结构,依次倒叙执行多个 defer 语句
	t.Log(" 「雪之梦技术驿站」: defer 底层实现数据结构类似于栈结构,依次倒叙执行多个 defer 语句")

	// 2 3 1
	defer t.Log(1)
	t.Log(2)
	defer t.Log(3)
}

初步认识了 defer 延迟函数的使用情况后,我们再结合文档详细解读一下相关定义.

  • 英文原版文档

> A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns,either because the surrounding function executed a return statement,reached the end of its function body,or because the corresponding goroutine is panicking.

  • 中文翻译文档

> "defer"语句调用一个函数,该函数的执行被推迟到周围函数返回的那一刻,这是因为周围函数执行了一个return语句,到达了函数体的末尾,或者是因为相应的协程正在惊慌.

具体来说,延迟函数的执行时机大概分为三种情况:

周围函数执行return

> because the surrounding function executed a return statement

return 后面的 t.Log(4) 语句自然是不会运行的,程序最终输出结果为 3 2 1 说明了 defer 语句会在周围函数执行 return 前依次逆序执行.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func funcWithMultipleDeferAndReturn() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	fmt.Println(3)
	return
	fmt.Println(4)
}

func TestFuncWithMultipleDeferAndReturn(t *testing.T) {
	// 「雪之梦技术驿站」: defer 延迟函数会在包围函数正常return之前逆序执行.
	t.Log(" 「雪之梦技术驿站」: defer 延迟函数会在包围函数正常return之前逆序执行.")

	// 3 2 1
	funcWithMultipleDeferAndReturn()
}

周围函数到达函数体

> reached the end of its function body

周围函数的函数体运行到结尾前逆序执行多个 defer 语句,即先输出 3 后依次输出 2 1. 最终函数的输出结果是 3 2 1 ,也就说是没有 return 声明也能保证结束前执行完 defer 延迟函数.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func funcWithMultipleDeferAndEnd() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	fmt.Println(3)
}

func TestFuncWithMultipleDeferAndEnd(t *testing.T) {
	// 「雪之梦技术驿站」: defer 延迟函数会在包围函数到达函数体结尾之前逆序执行.
	t.Log(" 「雪之梦技术驿站」: defer 延迟函数会在包围函数到达函数体结尾之前逆序执行.")

	// 3 2 1
	funcWithMultipleDeferAndEnd()
}

当前协程正惊慌失措

> because the corresponding goroutine is panicking

周围函数万一发生 panic 时也会先运行前面已经定义好的 defer 语句,而 panic 后续代码因为没有特殊处理,所以程序崩溃了也就无法运行.

函数的最终输出结果是 3 2 1 panic ,如此看来 defer 延迟函数还是非常尽忠职守的,虽然心里很慌但还是能保证老弱病残先行撤退!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func funcWithMultipleDeferAndPanic() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	fmt.Println(3)
	panic("panic")
	fmt.Println(4)
}

func TestFuncWithMultipleDeferAndPanic(t *testing.T) {
	// 「雪之梦技术驿站」: defer 延迟函数会在包围函数panic惊慌失措之前逆序执行.
	t.Log(" 「雪之梦技术驿站」: defer 延迟函数会在包围函数panic惊慌失措之前逆序执行.")

	// 3 2 1
	funcWithMultipleDeferAndPanic()
}

通过解读 defer 延迟函数的定义以及相关示例,相信已经讲清楚什么是 defer 延迟函数了吧?

简单地说,延迟函数就是一种未雨绸缪的规划机制,帮助开发者编程程序时及时做好收尾善后工作,提前做好预案以准备随时应对各种情况.

  • 当周围函数正常执行到到达函数体结尾时,如果发现存在延迟函数自然会逆序执行延迟函数.
  • 当周围函数正常执行遇到return语句准备返回给调用者时,存在延迟函数时也会执行,同样满足善后清理的需求.
  • 当周围函数异常运行不小心 panic 惊慌失措时,程序存在延迟函数也不会忘记执行,提前做好预案发挥了作用.

所以不论是正常运行还是异常运行,提前做好预案总是没错的,基本上可以保证万无一失,所以不妨考虑考虑 defer 延迟函数?

延迟函数应用场景

基本上成双成对的操作都可以使用延迟函数,尤其是申请的资源前后存在依赖关系时更应该使用 defer 关键字来简化处理逻辑.

下面举两个常见例子来说明延迟函数的应用场景.

  • Open/Close

文件操作一般会涉及到打开和开闭操作,尤其是文件之间拷贝操作更是有着严格的顺序,只需要按照申请资源的顺序紧跟着defer 就可以满足资源释放操作.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func readFileWithDefer(filename string) ([]byte, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	return ioutil.ReadAll(f)
}
  • Lock/Unlock

锁的申请和释放是保证同步的一种重要机制,需要申请多个锁资源时可能存在依赖关系,不妨尝试一下延迟函数!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var mu sync.Mutex
var m = make(map[string]int)
func lookupWithDefer(key string) int {
	mu.Lock()
	defer mu.Unlock()
	return m[key]
}

总结以及下节预告

defer 延迟函数是保障关键逻辑正常运行的一种机制,如果存在多个延迟函数的话,一般会按照逆序的顺序运行,类似于栈结构.

延迟函数的运行时机一般有三种情况:

  • 周围函数遇到返回时
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func funcWithMultipleDeferAndReturn() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	fmt.Println(3)
	return
	fmt.Println(4)
}
  • 周围函数函数体结尾处
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func funcWithMultipleDeferAndEnd() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	fmt.Println(3)
}
  • 当前协程惊慌失措中
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func funcWithMultipleDeferAndPanic() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	fmt.Println(3)
	panic("panic")
	fmt.Println(4)
}

本文主要介绍了什么是 defer 延迟函数,通过解读官方文档并配套相关代码认识了延迟函数,但是延迟函数中存在一些可能令人比较迷惑的地方.

读者不妨看一下下面的代码,将心里的猜想和实际运行结果比较一下,我们下次再接着分享,感谢你的阅读.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func deferFuncWithAnonymousReturnValue() int {
	var retVal int
	defer func() {
		retVal++
	}()
	return 0
}

func deferFuncWithNamedReturnValue() (retVal int) {
	defer func() {
		retVal++
	}()
	return 0
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

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

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
go 学习笔记之咬文嚼字带你弄清楚 defer 延迟函数
> 运行结果: 3 2 1 . > > 「雪之梦技术驿站」: defer fmt.Println(1) 和 defer fmt.Println(2) 两个语句由于前面存在 defer 关键字,因此均被延迟到正常语句 return 前.当多个 defer 语句均被延迟时,倒序执行延迟语句,这种特点非常类似于数据结构的栈(先入后出).所以依次输出 fmt.Println(3) ,defer fmt.Println(2) ,defer fmt.Println(1) .
雪之梦技术驿站
2019/11/20
5840
go 学习笔记之仅仅需要一个示例就能讲清楚什么闭包
本篇文章是 Go 语言学习笔记之函数式编程系列文章的第二篇,上一篇介绍了函数基础,这一篇文章重点介绍函数的重要应用之一: 闭包
雪之梦技术驿站
2019/10/01
4720
Golang语言延迟函数defer用法分析
本文实例讲述了GO语言延迟函数defer用法。分享给大家供大家参考。具体分析如下: defer 在声明时不会立即执行,而是在函数 return 后,再按照 FILO (先进后出)的原则依次执行每一个 defer,一般用于异常处理、释放资源、清理数据、记录日志等。这有点像面向对象语言的析构函数,优雅又简洁,是 Golang 的亮点之一。 代码1:了解 defer 的执行顺序 package main import "fmt" func fn(n int) int { defer func() { n+
李海彬
2018/03/21
8640
go 学习笔记之10 分钟简要理解 go 语言闭包技术
闭包是主流编程语言中的一种通用技术,常常和函数式编程进行强强联合,本文主要是介绍 Go 语言中什么是闭包以及怎么理解闭包.
雪之梦技术驿站
2019/10/01
4670
go 单元测试进阶篇
腾讯云数据库团队
2017/01/05
9K2
go 单元测试进阶篇
学习go语言编程之错误处理
将error作为多种返回值中的一个,但是这并非强制要求。 调用代码时建议按如下方式处理错误情况:
编程随笔
2023/10/15
1850
go 学习笔记之值得特别关注的基础语法有哪些
在上篇文章中,我们动手亲自编写了第一个 Go 语言版本的 Hello World,并且认识了 Go 语言中有意思的变量和不安分的常量.
雪之梦技术驿站
2019/08/20
7070
Go语言入门——进阶语法篇(四)
Go语言没有类似Java或Python那种try...catch...机制处理异常,Go的哲学是与众不同的,Go的设计者认为主流的异常处理机制是一种被过度滥用的技巧,而且存在很大的潜在危害,Go的异常处理(或者说是错误处理)是一种非常简单直观的方式。通常的,我们在写Java、Python之类的代码时,遇到可能存在的异常,直接用try括起来,使用catch捕获,然后就万事大吉了,当系统长时间的运行时,大大增加了不稳定性,所积累的问题可能在某一刻爆发。而Go者使用一种称为"恐慌的"机制,在有必要时,直接让系统宕机,让问题发生时立刻暴露出来,不必累积。很难说哪种设计更好,但Go语言确实简化了代码。
arcticfox
2019/09/03
5390
go 学习笔记之数组还是切片都没什么不一样
上篇文章中详细介绍了 Go 的基础语言,指出了 Go 和其他主流的编程语言的差异性,比较侧重于语法细节,相信只要稍加记忆就能轻松从已有的编程语言切换到 Go 语言的编程习惯中,尽管这种切换可能并不是特别顺畅,但多加练习尤其是多多试错,总是可以慢慢感受 Go 语言之美!
雪之梦技术驿站
2019/08/18
4140
Go - 从0学习Go的第一课
2.下载编辑器,Atom在github上是开源的,官网:https://github.com/atom
stark张宇
2023/03/07
3270
go 学习笔记之学习函数式编程前不要忘了函数基础 原
在编程世界中向来就没有一家独大的编程风格,至少目前还是百家争鸣的春秋战国,除了众所周知的面向对象编程还有日渐流行的函数式编程,当然这也是本系列文章的重点.
雪之梦技术驿站
2019/09/16
5740
PHP转Go速学手册
整理了一份简要的手册,帮助大家高效的上手Go语言,主要是通过对比PHP和Go的不同点来强化理解,内容主要分为以下四部分:
用户1093396
2021/07/28
2.4K0
【和博主一起去浪(golang)吧】文件操作详解(三)——简易文件拷贝和缓冲式文件拷贝
简易的文件拷贝 package main import ( "fmt" "io" "os" ) func main() { srcFile, _ := os.OpenFile("C:\\\\Users\\\\11316\\\\Desktop\\\\test.txt",os.O_RDONLY,0666) dstFile, _ := os.OpenFile("C:\\\\Users\\\\11316\\\\Desktop\\\\test1.txt",os.O_CREATE|os.O_WRONLY,
Regan Yue
2021/09/16
2580
golang--单元测试综合实例
(2)Monster有一个Store方法,可以将一个Monster对象序列化后保存在文件中;
西西嘛呦
2020/08/26
3400
Go语言开发小技巧&易错点100例(六)
打印日志的意义在于记录程序运行过程中的各种信息和事件,以便在程序出现问题时能够更快地定位和解决问题。日志可以记录程序的输入、输出、异常、错误、性能指标等信息,帮助开发人员和运维人员快速发现问题,进行调试和优化。此外,日志还能为程序运行提供审计和监控的功能,方便对程序的运行情况进行分析和评估。因此,打印日志是程序开发和维护中非常重要的一项工作。
闫同学
2023/09/29
1990
浅析golang中的defer
延迟执行可以用在很多的场景,比如连接数据库、打开文件、获取http连接等资源后,都需要释放资源,但是写代码的人容易忘记关闭资源的连接,且容易造成代码冗余。所以可以用defer语句在资源打开后马上调用defer去释放资源,可以避免忘记释放资源。因此,在诸如打开连接/关闭连接;申请/释放锁;打开文件/关闭文件等成对出现的操作场景里,defer会显得格外方便,如下:
素履coder
2022/02/17
5130
Golang之轻松化解defer的温柔陷阱
defer是Go语言提供的一种用于注册延迟调用的机制:让函数或语句可以在当前函数执行完毕后(包括通过return正常结束或者panic导致的异常结束)执行。
李海彬
2019/05/14
8310
【Golang】使用Golang编写Hugo发布器
有这么一种说法,懒人创造了世界。他们懒得走路,所以发明了汽车;懒得爬楼梯,所以发明了电梯;懒得扇扇子,所以发明了电风扇、空调。懒说明了怕麻烦,博主其实就是一个怕麻烦的人。博主的博客Garfield-加菲的博客就是通过Hugo自动生成的静态网站,首先强调一点,我喜欢Hugo,它使我能够专注于markdown的编写,其他一切事情都交给Hugo,这也符合我懒的特点。
DDGarfield
2022/06/23
9570
【Golang】使用Golang编写Hugo发布器
Golang之轻松化解defer的温柔陷阱
defer是Go语言提供的一种用于注册延迟调用的机制:让函数或语句可以在当前函数执行完毕后(包括通过return正常结束或者panic导致的异常结束)执行。深受Go开发者的欢迎,但一不小心就会掉进它的温柔陷阱,只有深入理解它的原理,我们才能轻松避开,写出漂亮稳健的代码。
梦醒人间
2019/05/21
4100
【笔记】Go Coding In Go Way
https://opensource.googleblog.com/2020/08/new-case-studies-about-googles-use-of-go.html
于顾而言SASE
2024/11/07
1340
【笔记】Go Coding In Go Way
相关推荐
go 学习笔记之咬文嚼字带你弄清楚 defer 延迟函数
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验