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
语句进行错误处理和资源回收,保证程序的正常运行和数据的安全性。同时,这种方式也使得代码更加简洁易读,提高了代码的可维护性和可读性。