Skip to content

1.13 流程控制:defer 延迟语句

defer 关键字用于延迟一个函数的执行,直到包含该 defer 语句的函数执行完毕(返回)时才执行。它主要用于确保资源在任何情况下都能被正确释放和清理。

一、defer 的基础概念

1. 语法与执行时机

语法defer functionCall()
执行时机在外层函数执行结束(return 之前,或发生 panic)时执行。

示例:

go
func main() {
    fmt.Println("开始执行 main 函数")

    defer fmt.Println("这是 defer 语句 1") // 延迟执行
    defer fmt.Println("这是 defer 语句 2") // 延迟执行

    fmt.Println("main 函数即将结束")
    // defer 语句在 main 函数 return 之前执行
}

// 输出顺序:
// 1. 开始执行 main 函数
// 2. main 函数即将结束
// 3. 这是 defer 语句 2  <-- 注意顺序是 LIFO
// 4. 这是 defer 语句 1

2. defer 的核心用途

defer 的主要目的是简化清理操作,确保无论函数是正常返回还是发生 panic,清理工作都会被执行。

场景示例操作
文件操作确保文件句柄被关闭:defer file.Close()
数据库连接确保数据库连接被关闭:defer db.Close()
锁操作确保互斥锁被释放:defer mu.Unlock()

二、defer 的执行机制

Go 语言将 defer 语句放入一个特殊的栈 栈(Stack) (LIFO)中。当外层函数返回时,这些延迟函数会按照后进先出 (LIFO) 的顺序依次执行。

规则: 声明的 defer 语句会执行。

go
func printNumbers() {
    for i := 0; i < 3; i++ {
        defer fmt.Println(i)
    }
}

func main() {
    printNumbers()
    // 输出顺序:2, 1, 0 (LIFO)
}

三、defer 语句的参数求值时机

这是 defer 最容易让人困惑的地方:defer 语句的参数是在 defer 语句被定义时立即求值的,而不是在它被执行时。

go
func main() {
    i := 1
    
    // defer 语句被定义时,i 的值是 1,所以参数 i 立即被求值并固定为 1。
    defer fmt.Println("defer 结果:", i) 

    i++ // i 的值现在是 2,但这不影响 defer 语句的参数 i。
    
    fmt.Println("main 结束前 i:", i) 
    
    // 输出顺序:
    // 1. main 结束前 i: 2
    // 2. defer 结果: 1  <-- 打印的是 i 声明时的值 1
}

💡 如果您想让 defer 使用变量的最终值:

传递一个指针或将 defer 语句放在一个闭包中。

go
i := 1
// 使用闭包,闭包捕获的是变量本身,而不是值
defer func() {
    fmt.Println("闭包结果:", i) // 此时 i 的值是 2
}()
i++
// 输出: 闭包结果: 2

四、deferpanic/recover

defer 机制是 Go 语言处理运行时错误(panic)的核心。即使函数在执行过程中发生 panic,所有已定义的 defer 语句也会在程序崩溃前依次执行。

1. 确保清理执行

go
func safeWrite(file *os.File) {
    defer file.Close() // 无论函数是正常返回还是 panic,文件都会关闭
    
    // 假设这里发生了一个 panic...
    // panic("写入失败") 
    // ...
}

2. 配合 recover() 捕获和处理 panic

recover() 函数只有在 defer 函数内部调用时才有效,它可以捕获最近一次发生的 panic,阻止程序崩溃,并返回 panic 的参数。

go
func handlePanic() {
    // defer 语句在外层函数返回时执行
    if r := recover(); r != nil {
        fmt.Printf("程序发生 panic,已捕获并恢复。错误信息: %v\n", r)
    }
}

func riskyFunction() {
    defer handlePanic() // 确保 handlePanic 在函数结束时执行
    
    fmt.Println("尝试执行可能出错的代码...")
    panic("致命的运行时错误!") // 触发 panic
    fmt.Println("这行代码永远不会执行")
}

func main() {
    riskyFunction()
    fmt.Println("程序恢复正常,继续执行...") 
    // 程序不会崩溃,会继续执行 main 函数的后续代码
}

五、总结与建议

特性描述最佳实践
LIFO 顺序延迟调用是后进先出栈式执行。确保 defer 语句成对出现(如 Lock 对应 Unlock)。
参数即时求值defer 语句的参数在定义时立即求值并固定。如果需要变量的最终值,请传递指针或使用闭包
资源清理保证资源(文件、锁、连接)在任何退出路径上都能被关闭。只要获取了资源,就立即定义 defer 来释放它。
错误处理配合 recover 使用,可以捕获 panic 并进行程序恢复。仅在需要从不可恢复的错误中恢复时使用 recover

这份教程详细解释了 defer 语句的机制、LIFO 顺序、参数求值时机以及与 panic/recover 的配合使用。