前言
初学 defer 语法时还蛮有趣,本文对 defer 的重新学习,从基础知识和高级用法的所有内容,深入其原理。
什么是 defer
在 Go 中, defer
是一个关键字,用于延迟函数的执行,当所在函数体都执行完成后开始执行。
func main() {
defer fmt.Println("hello")
fmt.Println("Tom")
}
// output
// Tom
// Hello
从输出结果可以看出,defer 语句的 println 是最后执行的。mian函数输出 Tom 函数执行完成后开始执行 defer 语句输出 Hello。
所以defer 常用于一些任务操作后最后清理或关闭任务的操作。比如关闭数据库连接、关闭文件,释放互斥体。
func UploadFile() error {
f, err := os.Open("20280808.txt")
if err != nil {
return err
}
defer f.Close()
// ...
}
defer 语句操作“关闭”操作时一般放在“打开”的附近,这样可以避免忘记关闭文件。因为有时候可能在一个长逻辑函数体内不用拉到检查是否“关闭”。重要的是,运行时发生错误,延迟函数也会在函数执行结束返回时被执行。因为当发生恐慌时,堆栈将被展开,并且延迟函数将按特定顺序执行。
defer 堆积
当在函数中使用很多 defer 语句时,它们就会按照“堆栈”顺序执行,先进后出,最后一个defer函数将先执行。
func main() {
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
}
// Output:
// 3
// 2
// 1
-----------------------------------------------
func B() {
defer fmt.Println(1)
defer fmt.Println(2)
A()
}
func A() {
defer fmt.Println(3)
defer fmt.Println(4)
}
// Output:
// 4
// 3
// 2
// 1
每次调用 defer 语句时,都会将该函数添加至当前 goroutine 链表的顶部,当函数返回时,它会遍历链表并按照上图所示的顺序执行每个链表。但是需要注意它不会执行 goroutine 链表中的所有 defer,它只运行返回函数中的 defer,因为我们的 defer 链表可能包含来自许多不同函数的许多 defer。
defer 、panic、 recover
在coding的过程中,我们不仅会遇到编译错误,还会遇到运行错误:除数为0.数组越界,nil 指针等问题都会导致程序 panic。 当出现Panic 就会停止当前 Goroutine的执行,同时也会执行当前 Goroutine中的defer 函数,然后就是程序崩溃。
所以为了解决因意外错误导致程序崩了,我们可以在defer 函数中使用 recover 函数来重新获取 panic。
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
panic("This is a panic")
}
// Output:
// Recovered: This is a panic
recover 函数必须在 defer函数中调用才能正常,如果直接使用 recover 将会出现panic。
// 错误示例
func main() {
defer recover()
panic("This is a panic")
}
那是否可以在defer 函数的内部函数中使用 recover函数呢?可以执行下面的源代码:
func custRecover() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}
func main() {
defer func() {
custRecover()
// ...
}()
panic("This is a panic")
}
会发现执行结果还是发生panic了。这是因为当法神panic时函数结束前执行了defer 函数,但是custRecover
本身是在panic发生后才被调用。此时已经抛出panic
,程序流程已经中断,custRecover
无法捕获到。
defer 函数的参数
func SumPrintln(a int) {
fmt.Println(a)
}
func main() {
a := 10
defer SumPrintln(a)
a = 20
}
你认为,上面的源码的执行结果是什么?正确的答案是 10。
这是因为当使用 defer 语句时,就会立即获取到值然后就按该值进行放在堆栈中等待执行。所以在后面参数变化是也不会影响已经在堆栈中的defer 函数。
如果要解决该问题,可以使用闭包或者传递内存地址来解决。
func main() {
a := 10
// 使用闭包方式解决
defer func() {
SumPrintln(a)
}()
// 使用内存地址
defer SumPrintln(&a)
a = 20
}
defer 错误处理
在一开始的例子中我们打开文件并关闭文件,defer f.close()
,但如果在close 的时候发生错误时就无法正常处理错误。可以通过捕获错误方式解决该问题。
defer func() {
if err := f.Close(); err != nil {
// 记录错误信息,例如写入日志文件
fmt.Println("Error closing file:", err)
}
}()
如果 defer 函数发生错误时一般不会函数的正常执行,因为defer
语句中的代码是在函数执行完毕后才执行的。
总之,defer
关键字可以帮助我们在函数执行完毕后执行一些重要的代码,提高代码的健壮性和可维护性。比如常使用的场景有:
- 资源释放:
defer
常用于释放函数中获取的资源,例如关闭文件、释放数据库连接等。即使函数发生 panic,也能确保资源被正确释放,避免资源泄漏。 - 清理工作:
defer
用于执行一些清理工作,例如删除临时文件、恢复文件状态等。 - 日志记录:
defer
用于记录函数执行的开始和结束时间,或者记录函数执行结果。 - 错误处理:
defer
用于在函数发生 panic 时执行一些错误处理操作,例如记录错误信息、恢复程序状态等。