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

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

出入成双有始有终

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

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

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

  1. 打开源文件
srcFile, err := os.Open("fib.txt")
if err != nil {
t.Error(err)
return
}
  1. 创建目标文件
dstFile, err := os.Create("fib.txt.bak")
if err != nil {
t.Error(err)
return
}
  1. 拷贝源文件到目标文件
io.Copy(dstFile, srcFile)
  1. 关闭目标文件
dstFile.Close()
srcFile.Close()
  1. 关闭源文件
srcFile.Close()

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

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 的文件拷贝
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 的文件拷贝
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 延迟函数前,普通函数正常运行:

func TestFuncWithoutDefer(t *testing.T) {
// 「雪之梦技术驿站」: 正常顺序
t.Log("「雪之梦技术驿站」: 正常顺序") // 1 2
t.Log(1)
t.Log(2)
}

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

func TestFuncWithDefer(t *testing.T) {
// 「雪之梦技术驿站」: 正常顺序执行完毕后才执行 defer 代码
t.Log(" 「雪之梦技术驿站」: 正常顺序执行完毕后才执行 defer 代码") // 2 1
defer t.Log(1)
t.Log(2)
}

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

func TestFuncWithMultipleDefer(t *testing.T) {
// 「雪之梦技术驿站」: 猜测 defer 底层实现数据结构可能是栈,先进后出.
t.Log(" 「雪之梦技术驿站」: 猜测 defer 底层实现数据结构可能是栈,先进后出.") // 3 2 1
defer t.Log(1)
defer t.Log(2)
t.Log(3)
}

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

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 前依次逆序执行.

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 延迟函数.

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 延迟函数还是非常尽忠职守的,虽然心里很慌但还是能保证老弱病残先行撤退!

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 就可以满足资源释放操作.

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

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

var mu sync.Mutex
var m = make(map[string]int)
func lookupWithDefer(key string) int {
mu.Lock()
defer mu.Unlock()
return m[key]
}

总结以及下节预告

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

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

  • 周围函数遇到返回时
func funcWithMultipleDeferAndReturn() {
defer fmt.Println(1)
defer fmt.Println(2)
fmt.Println(3)
return
fmt.Println(4)
}
  • 周围函数函数体结尾处
func funcWithMultipleDeferAndEnd() {
defer fmt.Println(1)
defer fmt.Println(2)
fmt.Println(3)
}
  • 当前协程惊慌失措中
func funcWithMultipleDeferAndPanic() {
defer fmt.Println(1)
defer fmt.Println(2)
fmt.Println(3)
panic("panic")
fmt.Println(4)
}

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

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

func deferFuncWithAnonymousReturnValue() int {
var retVal int
defer func() {
retVal++
}()
return 0
} func deferFuncWithNamedReturnValue() (retVal int) {
defer func() {
retVal++
}()
return 0
}

延伸阅读参考文档

如果本文对你有所帮助,不用赞赏,点赞鼓励一下就是最大的认可,顺便也可以关注下微信公众号「 雪之梦技术驿站 」哟!

go 学习笔记之解读什么是defer延迟函数的更多相关文章

  1. go 学习笔记之咬文嚼字带你弄清楚 defer 延迟函数

    温故知新不忘延迟基础 A "defer" statement invokes a function whose execution is deferred to the momen ...

  2. Hadoop学习笔记(2) ——解读Hello World

    Hadoop学习笔记(2) ——解读Hello World 上一章中,我们把hadoop下载.安装.运行起来,最后还执行了一个Hello world程序,看到了结果.现在我们就来解读一下这个Hello ...

  3. Hadoop源码学习笔记(1) ——第二季开始——找到Main函数及读一读Configure类

    Hadoop源码学习笔记(1) ——找到Main函数及读一读Configure类 前面在第一季中,我们简单地研究了下Hadoop是什么,怎么用.在这开源的大牛作品的诱惑下,接下来我们要研究一下它是如何 ...

  4. Python学习笔记之map、zip和filter函数

    这篇文章主要介绍 Python 中几个常用的内置函数,用好这几个函数可以让自己的代码更加 Pythonnic 哦 1.map map() 将函数 func 作用于序列 seq 的每一个元素,并返回处理 ...

  5. 初探C++运算符重载学习笔记<2> 重载为友元函数

    初探C++运算符重载学习笔记 在上面那篇博客中,写了将运算符重载为普通函数或类的成员函数这两种情况. 以下的两种情况发生.则我们须要将运算符重载为类的友元函数 <1>成员函数不能满足要求 ...

  6. Prometheus监控学习笔记之解读prometheus监控kubernetes的配置文件

    0x00 概述 Prometheus 是一个开源和社区驱动的监控&报警&时序数据库的项目.来源于谷歌BorgMon项目.现在最常见的Kubernetes容器管理系统中,通常会搭配Pro ...

  7. Go xmas2020 学习笔记 08、Functions, Parameters & Defer

    08-Functions, Parameters. functions. first class. function signatures. parameter. pass by value. pas ...

  8. python学习笔记~INI、REG文件读取函数(自动修复)

    引入configparser,直接read整个INI文件,再调用get即可.但需要注意的是,如果INI文件本身不太规范,就会报各种错,而这又常常不可避免的.本文自定义函数通过try...except. ...

  9. Web安全测试学习笔记-SQL注入-利用concat和updatexml函数

    mysql数据库中有两个函数:concat和updatexml,在sql注入时经常组合使用,本文通过学习concat和updatexml函数的使用方法,结合实例来理解这种sql注入方式的原理. con ...

随机推荐

  1. lambda表达式分类

    public class StreamTest { public static void main(String[] args) { createStream(); getForEach(); get ...

  2. Vue 前端uni-app多环境配置部署服务器的问题

    目录 前端Vue 针对问题 package.json描述 多环境部署 查看源码获取解决方案 转载请标明出处: http://dujinyang.blog.csdn.net/ 本文出自:[奥特曼超人的博 ...

  3. Net基础篇_学习笔记_第十二天_面向对象继承(命名空间 、值类型和引用类型)

    命名空间可以认为类是属于命名空间的. 解决类的重名问题,可以看做类的“文件夹”如果在当前项目中没有这个类的命名空间,需要我们手动的导入这个类所在的命名空间.1).用鼠标去点2).alt+shift+F ...

  4. 表达式树练习实践:C# 五类运算符的表达式树表达

    目录 表达式树练习实践:C# 运算符 一,算术运算符 + 与 Add() - 与 Subtract() 乘除.取模 自增自减 二,关系运算符 ==.!=.>.<.>=.<= 三 ...

  5. STL迭代器

    大部分ACM中使用的都是C/C++语言,但是说到C语言和C++语言的区别,却不知道. C++语言用于竞赛真的是非常方便的,里面有很多函数还有STL这个好东西,比C语言方便,比其他语言好理解. 在C语言 ...

  6. 前端 页面加载完成事件 - onload,五种写法

    在js和jquery使用中,经常使用到页面加载完成后执行某一方法.通过整理,大概是五种方式(其中有的只是书写方式不一样). 1:使用jQuery的$(function){}; 2:使用jquery的$ ...

  7. [Scala]集合中List元素转Tuple元素的函数迭代写法

    ____ 本文链接: https://www.cnblogs.com/senwren/p/Scala-Lis-2-Tuple.html —— Scala没有提供相应写法, 但迭代写法仍然可以做到. 有 ...

  8. SQL Server 内存优化表的索引设计

    测试的版本:SQL Server 2017 内存优化表上可以创建哈希索引(Hash Index)和内存优化非聚集(NONCLUSTERED)索引,这两种类型的索引也是内存优化的,称作内存优化索引,和基 ...

  9. Mysql 笔记二

    Mysql 笔记二 Mysql 笔记二 Table of Contents 1. 前言 2. Master Thread 工作方式 2.1. 主循环(loop) 2.2. 后台循(backgroup ...

  10. 阿里云安装RocketMQ

    说明: 我的阿里云是centos 6.9 jdk 1.8.0_192-b12(安装教程参照:https://www.cnblogs.com/kingsonfu/p/9801556.html) mave ...