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

Go defer的执行顺序

2021-06-043.6k 阅读

Go defer 的基本概念

在 Go 语言中,defer 语句用于延迟函数的执行。当一个函数执行到 defer 语句时,会将 defer 后的函数调用压入一个栈中,等到包含该 defer 语句的函数即将返回时,才会按照后进先出(LIFO,Last In First Out)的顺序依次执行这些被延迟的函数调用。

以下是一个简单的示例代码:

package main

import "fmt"

func main() {
    defer fmt.Println("defer 1")
    defer fmt.Println("defer 2")
    fmt.Println("main function")
}

在上述代码中,main 函数里有两个 defer 语句,分别延迟了两个 fmt.Println 函数的执行。当 main 函数执行到 fmt.Println("main function") 之后,函数即将返回,此时会按照后进先出的顺序执行被延迟的函数调用,因此输出结果为:

main function
defer 2
defer 1

defer 栈的实现原理

Go 语言的运行时(runtime)维护了一个栈结构来管理 defer 函数调用。当一个函数中遇到 defer 语句时,会将对应的 defer 函数调用信息封装成一个 deferproc 结构体,并压入这个栈中。这个结构体包含了要调用的函数指针、函数参数等信息。

当函数返回时,运行时会调用 deferreturn 函数,从 defer 栈中弹出 deferproc 结构体,并依次执行其中封装的函数调用。这种栈结构的实现确保了 defer 函数按照后进先出的顺序执行。

函数返回过程中 defer 的执行时机

defer 函数的执行时机是在包含它的函数即将返回时。这里需要注意的是,即使函数因为 panic 而异常终止,defer 函数依然会被执行。

考虑下面这个代码示例:

package main

import "fmt"

func divide(a, b int) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    result := a / b
    fmt.Println("Result:", result)
}

func main() {
    divide(10, 0)
    fmt.Println("After divide call")
}

divide 函数中,当 b 为 0 时,a / b 会引发 panic。但是由于存在 defer 函数,该 defer 函数中的 recover 可以捕获到这个 panic,使得程序不会崩溃。defer 函数在 panic 发生后,divide 函数即将异常返回时被执行。输出结果为:

Recovered from panic: runtime error: integer divide by zero
After divide call

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

当函数存在多层嵌套时,defer 的执行顺序依然遵循后进先出的原则。每一层函数的 defer 都会在该层函数返回时执行。

下面是一个多层嵌套函数的示例:

package main

import "fmt"

func outer() {
    fmt.Println("Enter outer")
    defer fmt.Println("Exit outer")
    inner()
}

func inner() {
    fmt.Println("Enter inner")
    defer fmt.Println("Exit inner")
    deeper()
}

func deeper() {
    fmt.Println("Enter deeper")
    defer fmt.Println("Exit deeper")
}

func main() {
    outer()
}

在上述代码中,outer 函数调用 inner 函数,inner 函数又调用 deeper 函数。每个函数都有自己的 defer 语句。执行结果如下:

Enter outer
Enter inner
Enter deeper
Exit deeper
Exit inner
Exit outer

可以看到,defer 函数在各自所在函数即将返回时,按照后进先出的顺序执行。

defer 与函数返回值的关系

在 Go 语言中,defer 函数可以访问函数的返回值,并且可以在函数返回前修改返回值。

考虑以下代码:

package main

import "fmt"

func calculate() (result int) {
    defer func() {
        result = result + 10
    }()
    result = 5
    return
}

func main() {
    value := calculate()
    fmt.Println("Calculated value:", value)
}

calculate 函数中,定义了一个返回值 resultdefer 函数在 calculate 函数即将返回时执行,它对 result 进行了修改。最终输出结果为:

Calculated value: 15

这表明 defer 函数可以在函数返回前访问并修改返回值。

复杂数据结构与 defer

当函数中涉及到复杂数据结构(如指针、结构体、切片等)时,defer 的行为需要特别注意。

指针与 defer

package main

import "fmt"

func modifyPointer(ptr *int) {
    defer func() {
        *ptr = *ptr + 10
    }()
    *ptr = 5
}

func main() {
    num := 0
    modifyPointer(&num)
    fmt.Println("Modified number:", num)
}

在上述代码中,modifyPointer 函数接受一个指向 int 类型的指针。defer 函数通过指针修改了所指向的值。最终输出结果为:

Modified number: 15

结构体与 defer

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func updatePerson(p *Person) {
    defer func() {
        p.Age = p.Age + 1
    }()
    p.Name = "New Name"
}

func main() {
    person := Person{Name: "Old Name", Age: 20}
    updatePerson(&person)
    fmt.Println("Updated person:", person)
}

在这个例子中,updatePerson 函数接受一个指向 Person 结构体的指针。defer 函数对结构体中的 Age 字段进行了修改。输出结果为:

Updated person: {New Name 21}

切片与 defer

package main

import "fmt"

func appendToSlice(slice []int) {
    defer func() {
        slice = append(slice, 100)
    }()
    slice = append(slice, 10)
}

func main() {
    mySlice := make([]int, 0)
    appendToSlice(mySlice)
    fmt.Println("Slice after append:", mySlice)
}

这里需要注意的是,虽然 defer 函数对 slice 进行了 append 操作,但由于 Go 语言中函数参数传递是值传递,defer 函数中的 slice 是原 slice 的副本。因此,最终输出结果为:

Slice after append: []

如果想要修改原切片,可以传递切片的指针:

package main

import "fmt"

func appendToSlice(slice *[]int) {
    defer func() {
        *slice = append(*slice, 100)
    }()
    *slice = append(*slice, 10)
}

func main() {
    mySlice := make([]int, 0)
    appendToSlice(&mySlice)
    fmt.Println("Slice after append:", mySlice)
}

此时输出结果为:

Slice after append: [10 100]

defer 与并发编程

在 Go 语言的并发编程中,defer 也有一些需要注意的地方。

在 goroutine 中使用 defer

每个 goroutine 都有自己独立的 defer 栈。当一个 goroutine 结束时,会执行其 defer 栈中的函数。

package main

import (
    "fmt"
    "time"
)

func worker() {
    defer fmt.Println("Worker finished")
    fmt.Println("Worker started")
    time.Sleep(2 * time.Second)
}

func main() {
    go worker()
    time.Sleep(3 * time.Second)
    fmt.Println("Main finished")
}

在上述代码中,worker 函数作为一个 goroutine 运行。worker 函数中的 defer 函数在 goroutine 结束时执行。输出结果为:

Worker started
Worker finished
Main finished

同步原语与 defer

在使用同步原语(如 sync.Mutex)时,defer 可以用于确保资源的正确释放。

package main

import (
    "fmt"
    "sync"
)

var mu sync.Mutex
var count int

func increment() {
    mu.Lock()
    defer mu.Unlock()
    count++
    fmt.Println("Incremented count:", count)
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            increment()
        }()
    }
    wg.Wait()
    fmt.Println("Final count:", count)
}

increment 函数中,使用 defer 来确保 mu.Unlock() 在函数结束时被调用,从而保证了对 count 变量的安全访问。

优化 defer 的使用

虽然 defer 非常方便,但过度使用或不合理使用可能会带来性能问题。

避免在循环中使用 defer

在循环中使用 defer 会导致大量的 defer 函数调用被压入栈中,增加栈的开销。

package main

import "fmt"

func badUsage() {
    for i := 0; i < 10000; i++ {
        defer fmt.Println(i)
    }
}

func main() {
    badUsage()
}

在上述代码中,每次循环都会将一个 fmt.Println 函数调用压入 defer 栈,这会消耗大量的栈空间和时间。

提前计算 defer 参数

如果 defer 函数的参数是一个复杂的表达式,建议提前计算好参数值,而不是在 defer 语句中计算。

package main

import (
    "fmt"
    "time"
)

func complexCalculation() int {
    time.Sleep(1 * time.Second)
    return 42
}

func betterUsage() {
    result := complexCalculation()
    defer fmt.Println(result)
    // 其他逻辑
}

func main() {
    start := time.Now()
    betterUsage()
    elapsed := time.Since(start)
    fmt.Println("Elapsed time:", elapsed)
}

betterUsage 函数中,提前调用 complexCalculation 并将结果保存到 result 变量中,避免了在 defer 语句执行时才进行复杂计算,从而提高了性能。

总结 defer 的执行顺序规则

  1. 后进先出(LIFO):在同一个函数中,defer 函数按照它们出现的逆序执行,即最后定义的 defer 函数最先执行。
  2. 函数返回前执行defer 函数在包含它们的函数即将返回时执行,无论是正常返回还是因为 panic 异常返回。
  3. 多层嵌套函数:每层函数的 defer 在该层函数返回时执行,同样遵循后进先出原则。
  4. 与返回值的关系defer 函数可以访问并修改函数的返回值,这使得在函数返回前进行一些清理或修正操作变得很方便。
  5. 复杂数据结构:对于指针、结构体等复杂数据结构,defer 函数对其的操作需要注意数据的传递方式(值传递还是指针传递),以确保达到预期效果。
  6. 并发编程:每个 goroutine 有自己独立的 defer 栈,defer 可与同步原语配合使用来保证资源的正确管理。
  7. 性能优化:应避免在循环中过度使用 defer,并提前计算 defer 函数的参数,以提高程序性能。

通过深入理解和合理运用 defer 的执行顺序规则,开发者可以编写出更加健壮、清晰和高效的 Go 语言程序。无论是资源管理、错误处理还是代码的可读性,defer 都发挥着重要的作用。在实际编程中,不断积累经验,根据具体场景选择最合适的 defer 使用方式,是提升 Go 编程能力的关键之一。