1. 简介
defer关键字,并对其进行基本介绍。接着,将详细介绍在资源回收、拦截和处理panic等相关场景下defer
的使用。
defer的执行顺序,以及在注册defer
函数时,其参数的求值时机等相关特性。最后,重点讲解defer
的注意点,如在defer
中函数中需要尽量避免引起panic,以及尽量避免在defer
中使用闭包。
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(
方法手动关闭了资源。
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
语句执行完后执行,以此类推。
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中使用闭包
在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是Go语言中一个非常方便和强大的语法特性,在某些场景下可以帮助我们更好地实现某些功能。但是,在使用defer时需要注意一些问题,避免引起不必要的麻烦。掌握defer的使用技巧,可以让我们的代码更加健壮、清晰和易于维护。