Go defer的执行顺序
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
函数中,定义了一个返回值 result
。defer
函数在 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 的执行顺序规则
- 后进先出(LIFO):在同一个函数中,
defer
函数按照它们出现的逆序执行,即最后定义的defer
函数最先执行。 - 函数返回前执行:
defer
函数在包含它们的函数即将返回时执行,无论是正常返回还是因为panic
异常返回。 - 多层嵌套函数:每层函数的
defer
在该层函数返回时执行,同样遵循后进先出原则。 - 与返回值的关系:
defer
函数可以访问并修改函数的返回值,这使得在函数返回前进行一些清理或修正操作变得很方便。 - 复杂数据结构:对于指针、结构体等复杂数据结构,
defer
函数对其的操作需要注意数据的传递方式(值传递还是指针传递),以确保达到预期效果。 - 并发编程:每个 goroutine 有自己独立的
defer
栈,defer
可与同步原语配合使用来保证资源的正确管理。 - 性能优化:应避免在循环中过度使用
defer
,并提前计算defer
函数的参数,以提高程序性能。
通过深入理解和合理运用 defer
的执行顺序规则,开发者可以编写出更加健壮、清晰和高效的 Go 语言程序。无论是资源管理、错误处理还是代码的可读性,defer
都发挥着重要的作用。在实际编程中,不断积累经验,根据具体场景选择最合适的 defer
使用方式,是提升 Go 编程能力的关键之一。