Go 语言 defer:你需要掌握的三个核心要点

4 阅读6分钟

摘要

defer 是 Go 语言中一个极具特色且功能强大的关键字。它能确保函数在返回前执行某些特定操作,常用于资源释放、解锁等场景。然而,defer 的一些行为特性,尤其是在与 return 和变量作用域结合时,常常会成为初学者的困惑点。

本文将通过清晰的代码示例,深入剖析 defer 的三个核心要点,帮助你彻底掌握它,并在实战中规避常见陷阱。

  1. 执行顺序:多个 defer 语句的执行顺序是怎样的?
  2. 与返回值交互deferreturn 和函数返回值三者间的执行时序和影响。
  3. 参数求值时机defer 后的函数参数是在声明时还是在执行时被求值的?

1. defer 的执行顺序:后进先出 (LIFO)

当一个函数内包含多个 defer 语句时,它们的执行顺序遵循“后进先出”(Last-In, First-Out, LIFO)的原则,就像栈(Stack)数据结构一样。最后声明的 defer 语句会最先被执行。

示例代码:

package main

import "fmt"

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

    defer func() {
        fmt.Println("defer 1: 我是最后一个声明的,所以最先执行。")
    }()

    defer func() {
        fmt.Println("defer 2: 我是第二个声明的。")
    }()

    defer func() {
        fmt.Println("defer 3: 我是第一个声明的,所以最后执行。")
    }()

    fmt.Println("函数即将结束")
}

执行输出:

函数开始
函数即将结束
defer 1: 我是最后一个声明的,所以最先执行。
defer 2: 我是第二个声明的。
defer 3: 我是第一个声明的,所以最后执行。

结论defer 的执行顺序与声明顺序相反,这对于理解和控制清理工作的顺序至关重要(例如,先解锁再关闭文件)。


2. defer、return 和返回值的交互

这是 defer 最微妙也最容易出错的地方。return 语句并非一个原子操作,它实际上包含了两个步骤:

  1. 赋值:将结果赋给返回值。
  2. 返回:函数带着返回值退出。

defer 的执行时机恰好在赋值之后,返回之前。根据函数返回值的定义方式(匿名或命名),defer 的行为会有所不同。

情况一:匿名返回值 (Anonymous Return Value)

当函数的返回值没有被显式命名时,return 语句会先将要返回的值存到一个临时的、函数外部不可见的变量中。defer 语句后续对任何局部变量的修改,都不会影响这个最终的返回值。

示例代码:

package main

import "fmt"

func main() {
    fmt.Println("最终结果:", test())
}

// 匿名返回值
func test() int {
    var i int = 0 // i 的初始值为 0

    defer func() {
        i++
        fmt.Println("defer 1, i =", i) // i 变为 1
    }()

    defer func() {
        i++
        fmt.Println("defer 2, i =", i) // i 变为 2
    }()

    // 关键步骤:
    // 1. return 将 i 的当前值 (0) 赋给一个临时返回值变量 (可理解为 ret_val = 0)
    // 2. 执行 defer 语句,修改局部变量 i 的值 (i 最终变为 2)
    // 3. 函数返回 ret_val,其值仍为 0
    return i
}

执行输出:

defer 2, i = 1  // 原文此处 defer1/2 顺序有误,已更正
defer 1, i = 2
最终结果: 0

详解return i 执行时,返回值 0 已经被确定并存好。虽然 defer 函数将局部变量 i 修改为了 2,但它无法触及那个已经存好的返回值。

情况二:命名返回值 (Named Return Value)

当函数使用命名返回值时,这个返回值变量在函数体内部是可见且可修改的。defer 语句对该变量的修改会直接影响函数的最终返回结果。

示例代码:

package main

import "fmt"

func main() {
    fmt.Println("最终结果:", test())
}

// 命名返回值 (i int)
func test() (i int) { // i 被初始化为 0
    defer func() {
        i++
        fmt.Println("defer 1, i =", i) // i 变为 2
    }()

    defer func() {
        i++
        fmt.Println("defer 2, i =", i) // i 变为 1
    }()

    // 关键步骤:
    // 1. return i 语句 (这里是裸 return),表示准备返回 i 的当前值 (0)
    // 2. 执行 defer 语句,它们直接操作的就是返回值变量 i
    // 3. defer 执行后,i 的值变为 2
    // 4. 函数返回 i 的最终值
    return i
}

执行输出:

defer 2, i = 1
defer 1, i = 2
最终结果: 2

详解:由于返回值 i 被命名,defer 函数闭包中引用的 i 和函数要返回的 i 是同一个变量。因此,defer 中的 i++ 操作直接修改了最终的返回值。


3. defer 的参数求值时机:声明时立即求值

这是一个非常关键的原则:defer 语句在声明时,其后的函数调用参数就已经被求值并保存了,而不是等到函数返回前才求值。

示例代码:

package main

import "fmt"

func main() {
    i := 1

    // 关键点1: 参数在 defer 声明时被求值
    // 此时 i 的值是 1,所以 defer 传入的是 1
    defer fmt.Println("defer 1: i 的值是", i)

    // 关键点2: 如果参数是指针,则地址被传入
    // defer 记下的是 i 的地址
    defer func(p *int) {
        fmt.Println("defer 2: 指针指向的值是", *p)
    }(&i)

    // 关键点3: 如果是闭包,它会捕获外部变量的引用
    // defer 了一个函数字面量 (闭包),它引用了变量 i
    defer func() {
        fmt.Println("defer 3: 闭包引用的 i 值是", i)
    }()

    i = 100 // 修改 i 的值

    fmt.Println("函数中: i 的最终值是", i)
}

执行输出:

函数中: i 的最终值是 100
defer 3: 闭包引用的 i 值是 100
defer 2: 指针指向的值是 100
defer 1: i 的值是 1

详解:

  • defer 1: 在声明时,i 的值是 1fmt.Println 的参数被立即求值为 1 并保存。后续 i 变为 100 与它无关。
  • defer 2: 在声明时,参数 &i 被求值,即 i 的内存地址。当 defer 执行时,它通过这个保存好的地址去取值,此时 i 的值已经是 100
  • defer 3: defer 的是一个闭包。闭包会捕获其外部变量的引用(地址)。当 defer 执行时,它通过引用访问 i,发现其值已经是 100

一种粗浅的理解defer的原理

package main

import "fmt"

// deferredCall 结构体没有变化
type deferredCall struct {
    function func(...interface{})
    args     []interface{}
}

// DeferManager 结构体没有变化
type DeferManager struct {
    calls []deferredCall
}

// NewDeferManager 函数没有变化
func NewDeferManager() *DeferManager {
    return &DeferManager{
       calls: make([]deferredCall, 0),
    }
}

// Defer 方法没有变化
func (dm *DeferManager) Defer(f func(...interface{}), args ...interface{}) {
    fmt.Printf("  [模拟 defer 注册] -> 函数被注册, 参数 %v 已被求值并复制。\n", args)
    call := deferredCall{
       function: f,
       args:     args,
    }
    dm.calls = append(dm.calls, call)
}

// Run 方法没有变化
func (dm *DeferManager) Run() {
    fmt.Println("  [模拟函数返回] -> 开始按 LIFO 顺序执行所有 deferred 调用...")
    for i := len(dm.calls) - 1; i >= 0; i-- {
       call := dm.calls[i]
       call.function(call.args...)
    }
}

// ----- 下面我们使用这个模拟器 (注意看这部分的变化) -----

func simulatedFunction() {
    dm := NewDeferManager()
    defer dm.Run()

    fmt.Println("模拟的函数开始执行...")

    i := 1
    s := "hello"

    // 【修正】创建一个包装函数,它的签名是 func(...interface{}),符合要求。
    // 这个包装函数内部会调用真正的 fmt.Println,并忽略其返回值。
    printWrapper := func(args ...interface{}) {
       fmt.Println(args...)
    }

    // 使用包装函数来调用 Defer
    dm.Defer(printWrapper, "第一次 defer:", i)
    dm.Defer(printWrapper, "第二次 defer:", s)

    fmt.Println("\n...模拟的函数体继续执行...")
    i = 100
    s = "goodbye"
    fmt.Printf("...函数体中,变量被修改为 i=%d, s="%s"\n\n", i, s)

    dm.Defer(printWrapper, "第三次 defer:", i)
}

func main() {
    simulatedFunction()
}

总结

为了高效、正确地使用 defer,请牢记以下三点:

  1. LIFO 顺序defer 的执行顺序与声明顺序相反。
  2. 返回值交互deferreturn 赋值之后、返回之前执行。它可以修改命名返回值,但无法修改匿名返回值
  3. 参数立即求值defer 语句的函数调用参数在声明时就被确定下来,而不是在执行时。