MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Go语言defer执行顺序的探究

2023-03-124.3k 阅读

Go 语言 defer 执行顺序的基本规则

在 Go 语言中,defer语句用于延迟函数的执行,直到包含该defer语句的函数返回。defer语句的典型用途包括资源清理,如关闭文件、数据库连接等。其基本执行顺序遵循“后进先出”(Last In First Out,LIFO)的栈规则。

简单示例

package main

import "fmt"

func main() {
    fmt.Println("Start")
    defer fmt.Println("First defer")
    defer fmt.Println("Second defer")
    fmt.Println("End")
}

在上述代码中,main函数开始执行后,首先输出"Start",然后遇到两个defer语句。这两个defer语句被压入栈中,按照出现的顺序,"First defer"先入栈,"Second defer"后入栈。接着执行fmt.Println("End")。当main函数执行完毕准备返回时,defer语句开始按照栈的 LIFO 顺序执行,所以先输出"Second defer",再输出"First defer"。最终输出结果为:

Start
End
Second defer
First defer

多层嵌套函数中的 defer 执行顺序

defer语句出现在多层嵌套函数中时,仍然遵循 LIFO 规则。

package main

import "fmt"

func outer() {
    fmt.Println("Outer start")
    inner()
    fmt.Println("Outer end")
}

func inner() {
    fmt.Println("Inner start")
    defer fmt.Println("Inner defer")
    fmt.Println("Inner end")
}

在这个例子中,outer函数调用inner函数。inner函数中有一个defer语句。当outer函数执行时,输出"Outer start",然后调用inner函数。inner函数输出"Inner start""Inner end",此时inner函数中的defer语句被压入栈。inner函数返回后,outer函数继续执行,输出"Outer end"。最后,inner函数中的defer语句按照 LIFO 规则执行,输出"Inner defer"。输出结果如下:

Outer start
Inner start
Inner end
Outer end
Inner defer

defer 与函数返回值的关系

命名返回值与 defer

当函数的返回值被命名时,defer语句可以访问和修改这些返回值。

package main

import "fmt"

func namedReturn() (result int) {
    result = 1
    defer func() {
        result++
    }()
    return result
}

在上述代码中,namedReturn函数有一个命名返回值result,初始值为 1。defer语句中的匿名函数在函数返回前执行,它将result的值加 1。因此,函数最终返回的值是 2。

未命名返回值与 defer

如果函数返回值未命名,defer语句虽然也会在函数返回前执行,但无法直接修改返回值。

package main

import "fmt"

func unnamedReturn() int {
    var temp int = 1
    defer func() {
        temp++
    }()
    return temp
}

在这个例子中,unnamedReturn函数返回一个未命名的int类型值。defer语句中的匿名函数将局部变量temp加 1,但这并不会影响函数的返回值。因为函数返回的是return语句处temp的值,而defer语句在return之后执行。所以函数返回的值仍然是 1。

defer 与 panic 和 recover

defer 在 panic 时的执行

当函数中发生panic时,defer语句仍然会按照 LIFO 顺序执行。

package main

import "fmt"

func panicWithDefer() {
    defer fmt.Println("First defer")
    defer fmt.Println("Second defer")
    panic("Panic occurred")
    fmt.Println("This line will not be printed")
}

在上述代码中,panicWithDefer函数中发生了panic。在panic发生后,函数立即停止正常执行,但defer语句会按照 LIFO 顺序执行。所以先输出"Second defer",再输出"First defer",最后打印出"Panic occurred"。输出结果为:

Second defer
First defer
panic: Panic occurred

goroutine 1 [running]:
main.panicWithDefer()
        /path/to/your/file.go:8 +0x8c
main.main()
        /path/to/your/file.go:14 +0x20

使用 recover 恢复 panic 并结合 defer

recover函数用于恢复panic状态,并且通常与defer语句一起使用。

package main

import "fmt"

func recoverFromPanic() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    panic("Panic in recoverFromPanic")
    fmt.Println("This line will not be printed")
}

在这个例子中,recoverFromPanic函数中有一个defer语句,其中的匿名函数使用recover函数来捕获panic。当panic发生时,defer语句执行,recover函数捕获到panic的值并输出"Recovered from panic: Panic in recoverFromPanic"

复杂场景下 defer 执行顺序的探究

多个 defer 语句与循环

defer语句在循环中使用时,需要注意其执行顺序。

package main

import "fmt"

func deferInLoop() {
    for i := 0; i < 3; i++ {
        defer fmt.Println("Defer in loop:", i)
    }
    fmt.Println("Loop ended")
}

在上述代码中,defer语句在循环内部。每次循环迭代,defer语句将一个函数调用压入栈中,这些函数调用会记住i的值。当循环结束后,fmt.Println("Loop ended")执行。然后,defer语句按照 LIFO 顺序执行,由于defer语句记住了循环结束时i的值,所以输出结果为:

Loop ended
Defer in loop: 2
Defer in loop: 1
Defer in loop: 0

defer 与闭包和变量作用域

闭包与defer结合时,变量作用域的问题需要特别关注。

package main

import "fmt"

func deferWithClosure() {
    var num int = 10
    defer func() {
        fmt.Println("Defer closure:", num)
    }()
    num = 20
    fmt.Println("Main function:", num)
}

在这个例子中,defer语句中的闭包引用了外部变量num。由于闭包捕获的是变量的引用,而不是值的副本,所以当num的值在defer语句之后被修改为 20 时,defer语句执行时输出的是修改后的值。输出结果为:

Main function: 20
Defer closure: 20

优化 defer 的使用

避免不必要的 defer

虽然defer语句在资源清理等方面非常方便,但过多使用defer可能会导致性能问题。例如,在一个循环中,如果每次迭代都使用defer来清理资源,可能会造成栈的过度增长。在这种情况下,可以考虑在循环外部进行资源清理。

package main

import (
    "fmt"
    "os"
)

// 不推荐的写法
func badDeferInLoop() {
    for i := 0; i < 1000; i++ {
        file, err := os.Open("test.txt")
        if err != nil {
            fmt.Println("Error opening file:", err)
            return
        }
        defer file.Close()
        // 处理文件
    }
}

// 推荐的写法
func goodDeferInLoop() {
    file, err := os.Open("test.txt")
    if err != nil {
        fmt.Println("Error opening file:", err)
        return
    }
    defer file.Close()
    for i := 0; i < 1000; i++ {
        // 处理文件
    }
}

提前返回与 defer

在函数中,如果有多个提前返回的地方,确保defer语句在合适的位置,以避免资源泄漏。

package main

import (
    "fmt"
    "os"
)

func earlyReturnWithDefer() {
    file, err := os.Open("test.txt")
    if err != nil {
        fmt.Println("Error opening file:", err)
        return
    }
    defer file.Close()
    // 其他逻辑
    if someCondition() {
        return
    }
    // 更多逻辑
}

func someCondition() bool {
    // 一些条件判断
    return true
}

在上述代码中,file.Close()defer语句在文件打开之后立即定义,确保无论函数在何处提前返回,文件都会被关闭。

总结 defer 执行顺序的要点

  1. 基本 LIFO 规则defer语句遵循后进先出的栈规则,在包含它的函数返回时执行。
  2. 与返回值的关系:命名返回值可以在defer语句中被修改,而未命名返回值则不能。
  3. 与 panic 和 recoverdeferpanic时仍然执行,recover可与defer结合恢复panic
  4. 复杂场景:在循环和闭包中使用defer时,要注意变量作用域和执行顺序。
  5. 优化使用:避免不必要的defer,合理安排defer语句以提高性能并防止资源泄漏。

通过深入理解 Go 语言中defer的执行顺序,开发者可以更加高效、安全地编写代码,尤其是在处理资源管理和异常处理等方面。在实际项目中,正确使用defer能够显著提升代码的健壮性和可读性。

希望以上内容能帮助你深入理解 Go 语言中defer的执行顺序。在日常开发中,不断实践和总结,能更好地掌握这一重要特性。