共计 5427 个字符,预计需要花费 14 分钟才能阅读完成。
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 的应用技巧,能够让咱们的代码更加强壮、清晰和易于保护。