defer有什么用呢
1. 简介
本文将从一个资源回收问题引入,引出defer关键字,并对其进行基本介绍。接着,将详细介绍在资源回收、拦截和处理panic等相关场景下defer的使用。
进一步,介绍defer的执行顺序,以及在注册defer函数时,其参数的求值时机等相关特性。最后,重点讲解defer的注意点,如在defer中函数中需要尽量避免引起panic,以及尽量避免在defer中使用闭包。
通过本文的阅读,读者将对Go语言中的defer有更深入的了解,并且能够更加有效地使用这个关键字。
2. 问题引入
开发过程中,函数可能会打开文件、建立网络连接或者其他需要手动关闭的资源。当函数在处理过程中发生错误时,我们需要手动释放这些资源。而如果有多处需要进行错误处理,手动释放资源将是一个不小的心智负担。同时,如果我们遗漏了资源的释放,就会导致资源泄漏的问题。这种问题可能会导致系统性能下降、程序运行异常或者系统崩溃等。
以下是一个示例代码,其中函数打开了一个文件,读取其中的内容并返回:
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
var content []byte
_, err = f.Read(content)
if err != nil {
// 出现错误,此时调用Close释放资源
f.Close()
return nil, err
}
// 正常处理结束,也需要调用Close释放资源
f.Close()
return content, nil
}
在上面的代码中,我们使用了 os.Open 函数打开一个文件,并在函数返回之前使用 f.Close() 函数手动关闭文件。同时,在出现错误时,我们也调用了f.Close()方法手动关闭了资源。
但是,我们设想一下,如果函数中不仅仅只打开了一个文件,而是同时打开了文件,网络连接,数据库连接等资源,同时假设函数中需要错误处理的地方有5处,此时在错误处理中,来实现对资源的回收是非常大的心智负担,而且一旦在某个错误处理中,忘记对资源的回收,那就代表着资源的泄漏,将会带来一系列的问题。而且,如果在函数执行过程中发生了panic,此时将不会执行错误处理函数,会直接退出,函数打开的文件可能将不会被关闭。
综上所述,我们这里遇到的问题,在于函数处理过程中,会打开一些资源,在函数退出时需要正确释放资源。而释放资源的方式,如果是在每一个错误处理处来对资源进行释放,此时对于开发人员是一个不小的负担;同时对于函数执行过程中发生panic的情况,也无法正常释放资源。
那有什么方式,能够简洁高效得释放资源,无需在函数的多个错误处理处都执行一次资源的回收;同时也能够处理panic可能导致资源泄漏的问题吗? 其实还真有,Go中的defer关键字便非常适合在该场景中使用,下面我先来了解了解defer。
3. defer对问题的解决
3.1 defer基本介绍
在Go语言中,我们可以在函数体中使用 defer 关键字,来延迟函数或方法的执行。defer 延迟的函数或方法,会在当前函数执行结束时执行,无论函数是正常返回还是异常返回。也就是说,无论在函数中的哪个位置,只要使用了 defer 延迟执行了某个函数或方法,那么这个函数或方法的执行都会被推迟到当前函数执行结束时再执行。
defer 语句的语法很简单,它只需要在需要延迟执行的语句前加上 defer 关键字即可。defer 语句支持执行函数调用和方法调用,也可以在语句中使用函数参数和方法参数等。下面是一个 defer 语句的示例:
func demo() {
defer fmt.Println("deferred")
fmt.Println("hello")
}
在上面的示例中,我们使用了 defer 关键字,延迟了 fmt.Println("deferred") 的执行。当函数执行到 defer 语句时,这个语句并不会立即执行,而是被压入一个栈中,等到函数执行结束时,再按照后进先出的顺序依次执行这些被延迟的语句。在这个示例中,fmt.Println("hello") 会先被执行,然后是被延迟的 fmt.Println("deferred")。因此,输出的结果是:
hello
deferred
3.2 defer对上述问题的解决
通过上述描述,我们了解defer函数能够在函数或方法结束前延迟执行,而且无论函数是正常返回还是发生了panic,defer函数都会被执行。
这个特性非常适合用于资源的释放,例如打开的文件、建立的网络连接、申请的内存等等。我们可以在函数或方法中使用defer来延迟释放这些资源,从而避免因为忘记释放而导致的问题,同时也能够在发生异常时正确地释放资源,让代码更加健壮。下面我们使用defer对上面ReadFile函数进行改进,具体做法是在函数中使用defer关键字,将f.Close()操作延迟到函数结束时执行,代码如下:
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
// 获取到一个资源,便注册资源释放函数
defer f.Close()
var content []byte
_, err = f.Read(content)
if err != nil {
return nil, err
}
return content, nil
}
在之前的实现中,无论是正常结束还是出现错误,都需要调用f.Close()释放资源。而现在只需要通过defer关键字注册f.Close()函数即可,这样的代码更简洁,更容易维护,并且不会出现资源泄露的问题。
4.defer其他常见用途
defer语句除了用于在函数中释放资源外,还有其他一些场景的用途,如拦截和处理panic,用于函数结束时打印日志等内容,下面将仔细对其进行说明。
4.1 拦截和处理panic
使用defer语句可以在程序出现panic时,及时进行资源回收和错误处理,避免程序因未处理的panic而直接崩溃。具体来说,可以通过在函数开头使用defer语句注册一个函数来捕获panic。当发生panic时,程序会先执行defer语句注册的函数,再进行panic的传递。
例如下面的代码中,函数中使用了defer来捕获panic,并在发生panic时进行了错误处理和资源回收:
func someFunction() {
defer func() {
if r := recover(); r != nil {
log.Println("Recovered from panic:", r)
// 进行错误处理或者资源回收
}
}()
// 函数代码
// 可能会出现panic的代码
}
使用defer语句拦截和处理panic的好处是,在出现panic时,程序不会立即崩溃,而是可以通过defer语句进行错误处理和资源回收,保证程序的正常运行和数据的安全性。同时,这种方式也使得代码更加简洁易读,提高了代码的可维护性和可读性。
4.2 实现函数执行时间的计算
在性能测试和优化过程中,我们通常需要知道某个函数或代码段的执行时间。这个时候可以使用defer记录函数执行开始和结束的时间戳,然后计算两者之差,即可得到函数的执行时间。如下:
func foo() {
defer func() {
fmt.Println("foo execution time:", time.Since(start))
}()
start := time.Now()
// 函数执行逻辑
}
在上述代码中,我们使用time.Now()函数获取当前时间戳,并将其存储在start变量中。然后,在函数执行结束时,我们在defer语句中定义一个匿名函数,用来计算函数执行时间并输出。在匿名函数中,我们调用time.Since(start)函数来获取当前时间戳与start变量之间的时间差,并将其输出。这样可以帮助我们快速发现程序中耗时较长的代码段,进而进行优化。
总的来说,defer的场景用途还是比较广泛的,可以在需要在函数执行结束后执行某些操作的场景下使用。
5. defer相关特性
5.1 defer的执行顺序
当函数中有多个defer语句时,它们的执行顺序是后进先出的,也就是说最后一个defer语句会最先执行,倒数第二个defer语句会在最后一个defer语句执行完后执行,以此类推。
例如,下面的代码中有三个defer语句:
func main() {
defer fmt.Println("Third")
defer fmt.Println("Second")
defer fmt.Println("First")
fmt.Println("Hello, defer!")
}
当函数返回时,它们按照后进先出的顺序执行,所以输出结果是:
Hello, World!
First
Second
Third
5.2 注册defer函数时,其参数的求值时机
在注册defer函数时,如果defer函数传入的参数是变量,那么变量的求值顺序与普通函数调用一样,是在函数参数传递之前进行的。例如,假设有如下代码:
func foo() {
a := 1
defer func(x int) {
fmt.Println("x in defer:", x)
}(a)
a = 2
fmt.Println("a before end of function:", a)
}
在这个例子中,变量a在defer函数中被作为参数传递,defer语句中的匿名函数会捕获a的值,并在函数执行结束时打印该值。foo函数执行的结果如下:
a before end of function:2
x in defer:1
因此,可以看出在defer语句中传入的变量是在注册defer函数时进行求值的,而不是在函数执行结束时。
6. defer注意点
6.1 在defer中尽量避免执行可能引起panic的操作
在使用defer语句时,应当尽量避免在其中引起panic。因为当在defer语句中发生panic时,当前defer函数中后续的语句将无法得到执行,可能无法释放已经申请的资源。此时,程序可能会因为资源泄漏等问题而崩溃或产生其他不可预期的后果。举个例子,假设有如下代码:
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in defer:", r)
}
}()
fmt.Println("Start")
defer fmt.Println("First Defer")
defer func() {
fmt.Println("Second Defer")
panic("oops")
fmt.Println("资源回收")
}()
fmt.Println("End")
}
这段代码中,我们在第三个defer语句中引发了panic,这时会触发panic机制,第三个defer后续的代码将不会被执行,最后程序会输出如下结果:
Start
End
Second Defer
First Defer
Recovered in defer: oops
可以看到,第三个defer语句中,由于panic导致了fmt.Println("资源回收")语句无法被执行。因此,在编写代码时,我们应该尽量避免在defer中引起panic,如果不可避免有panic可能性的出现,此时应该对其进行处理,以确保程序的稳定性和可靠性。
6.2 尽量避免在defer中使用闭包
这里先简单介绍下闭包,在 Go 中,闭包是一个函数值(function value),它引用了函数体之外的变量。这个被引用的变量会被“捕获”到闭包中,即使这个变量在闭包被创建之后发生了变化,闭包中也能访问到变化后的值。
在defer中使用闭包可能会导致一些意想不到的问题。因为闭包引用了外部变量,而在defer函数执行时,这些变量的值可能已经被修改或者不再存在,从而导致出现不可预期的行为。
举个例子,假设有一个defer函数使用了闭包来记录当前时间戳和某个变量的值:
func foo() {
i := 0
defer func() {
fmt.Printf("i: %d, timestamp: %d\n", i, time.Now().UnixNano())
}()
i++
}
在这个例子中,我们使用了闭包来捕获了变量i和当前时间戳,并在defer函数中输出它们的值。然而,由于defer函数的执行时机是在函数返回之后,我们无法确定变量i的值是否已经被修改了。因此,这个例子可能输出的结果是不稳定的,无法得到预期的结果。
因此,尽量避免在defer中使用闭包,可以避免一些潜在的问题。如果必须要使用闭包,那么要格外小心,确保在defer函数执行时闭包引用的变量值仍然是符合预期的。
7. 总结
在本文中,我们从一个资源回收的问题引出了defer,介绍了defer的基本用法以及在资源回收、拦截和处理panic等场景中的使用。我们还讨论了defer的一些特性,如执行顺序以及注册defer函数时,参数的求值时机。最后,我们提醒了在使用defer时需要注意的一些问题,如尽量避免在defer中引起panic和避免在defer中使用闭包。
总的来说,defer是Go语言中一个非常方便和强大的语法特性,在某些场景下可以帮助我们更好地实现某些功能。但是,在使用defer时需要注意一些问题,避免引起不必要的麻烦。掌握defer的使用技巧,可以让我们的代码更加健壮、清晰和易于维护。
defer有什么用呢的更多相关文章
- 浅谈Angular的 $q, defer, promise
浅谈Angular的 $q, defer, promise 时间 2016-01-13 00:28:00 博客园-原创精华区 原文 http://www.cnblogs.com/big-snow/ ...
- javascript的defer和async的区别。
我们常用的script标签,有两个和性能.js文件下载执行相关的属性:defer和async defer的含义[摘自https://developer.mozilla.org/En/HTML/Elem ...
- script标签中defer和async属性的区别
这篇文章来源于JS高级程序设计第三版中关于script标签的介绍,结合查阅的资料写下的学习笔记. 向html页面中插入javascript代码的主要方法就是通过script标签.其中包括两种形式,第一 ...
- twitter.common.concurrent deadline and defer
此defer非golang中的defer https://tour.golang.org/flowcontrol/12 from twitter.common.concurrent import Ti ...
- script async 和script defer的区别
浏览器对js文件的操作主要有两部分:下载和执行: js文件下载在有些浏览器中是并行的,在有些浏览器中是串行的,如:IE8.firefox3.chrome2都是串行下载的: 执行在所有浏览器中默认是阻塞 ...
- angular中的$q.defer()服务异步处理
jquery和angular都有defer服务,我暂以angular为例谈谈我的理解,最后并附上jquery的阮一峰总结的defer. 以我目前项目的部分代码为例说明为什么要用deferred. fu ...
- Go语言异常处理defer\panic\recover
Go语言追求简洁优雅,所以,Go语言不支持传统的 try…catch…finally 这种异常,因为Go语言的设计者们认为,将异常与控制结构混在一起会很容易使得代码变得混乱.因为开发者很容易滥用异常, ...
- 浅谈JavaScript中的defer,async
引言 开始重读<<JavaScript高级程序设计>>一书,看到关于JavaScript中关于defer.async的部分.网上查询了点资料,觉得蛮好的.现在总结下. defe ...
- 【Go入门教程3】流程(if、goto、for、switch)和函数(多个返回值、变参、传值与传指针、defer、函数作为值/类型、Panic和Recover、main函数和init函数、import)
这小节我们要介绍Go里面的流程控制以及函数操作. 流程控制 流程控制在编程语言中是最伟大的发明了,因为有了它,你可以通过很简单的流程描述来表达很复杂的逻辑.Go中流程控制分三大类:条件判断,循环控制和 ...
- go:defer
defer:延迟. 假设有调用函数A.被调用函数B,其关系如下: func A(){//调用函数 ... defer B()//被调用函数 ... return//B将延迟到return前执行 } * ...
随机推荐
- SpringBoot笔记--自动配置(高级内容)(上集)
原理分析 自动配置 Condition--增加的条件判断功能 来一个案例说明: 具体实现: 没有要求的话,就是这样的: Config.java User.java SpringLearnApplica ...
- 程序员必须掌握的java进制转换(全网最详细讲解)
前言 在上一篇文章中,壹哥给大家讲了Java里的各种运算符.其中在讲解位运算符时,我给大家提到了计算机中进制的概念.但是现在很多小白同学,对进制的概念还不够了解,比如二进制.八进制.十六进制都是怎么回 ...
- Web 前端入门 JS 基础知识梳理汇总
Web 前端工程师是当前各大企业都比较稀缺的人才,薪资待遇和就业前景都很不错.不论是专业还是非专业,有基础亦或是无基础,都想通过学习 Web 前端实现高薪就业.不过,学习要一步一个脚印,不能一口吃一个 ...
- java注解与反射--2
java注解与反射--2 反射:java.Reflection 因为反射,使java具有了一定的动态性. java反射机制概述 动态语言: 是一类在运行时可以改变其结构的语言:例如新的函数.对象.甚至 ...
- SpringBoot——定制错误页面及原理
更多内容,前往 IT-BLOG 一.SpringBoot 默认的错误处理机制 [1]浏览器返回的默认错误页面如下: ☞ 浏览器发送请求的请求头信息如下:text/html 会在后面的源码分析中说到 ...
- Mybatis分页插件PageHelper的配置及使用方法
尊重人家的知识成果 推荐 该作者总结的不错! --->> --->> @author 扎心了老铁 Mybatis分页插件PageHelper的配置及使用方法
- python中socket使用UDP协议简单实现服务端与客户端通信
UDP为不可靠传输,也就是发送方不关心对方是否收到消息,一般用于聊天软件.但现在的聊天软件虽然使用的是UDP协议,但已从代码层面上解决了丢失信息的问题. 下面使用python代码简单实现了服务端与客户 ...
- python入门教程之二环境搭建
环境搭建 1python解释器 当我们编写Python代码时,我们得到的是一个包含Python代码的以.py为扩展名的文本文件.要运行代码,就需要Python解释器去执行.py文件. 由于整个Pyth ...
- 如何在Java中做基准测试?JMH使用初体验
大家好,我是王有志,欢迎和我聊技术,聊漂泊在外的生活.快来加入我们的Java提桶跑路群:共同富裕的Java人. 最近公司在搞新项目,由于是实验性质,且不会直接面对客户的项目,这次的技术选型非常激进,如 ...
- [Java EE] java.net.SocketException: Connection reset【解决中】
1 错误描述 6:44:33.112] [DEBUG] [http-nio-9527-exec-3] [HttpClientUtil] http post url:http://bdp-gateway ...