Go 语言中 defer 的使用指南

世间万物都别等失去了,再来睹物思人

Posted by yishuifengxiao on 2024-09-19

基本特性与语法

执行顺序(LIFO)

多个defer按后进先出(LIFO)顺序执行:

defer fmt.Println("A") // 最后执行
defer fmt.Println("B") // 第二执行
defer fmt.Println("C") // 最先执行
// 输出:C B A

参数求值时机

defer注册时立即求值参数,后续变量修改不影响已注册的值:

x := 1
defer fmt.Println(x) // 输出1(注册时x=1)
x = 2

执行时机

return赋值后、函数返回前执行,可修改具名返回值:

func demo() (result int) {
defer func() { result += 100 }()
return 1 // 实际返回101
}

主要使用场景

资源清理(最常见场景)

确保打开的文件、网络连接等资源被正确关闭

func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // 确保文件关闭

// 处理文件内容...
return nil
}

解锁互斥锁

确保锁在函数退出时被释放

var mu sync.Mutex
var data = make(map[string]string)

func updateData(key, value string) {
mu.Lock()
defer mu.Unlock() // 确保解锁

data[key] = value
}

捕获和处理 panic

与 recover 配合使用实现 panic 恢复

func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()

// 可能引发 panic 的代码
riskyOperation()
}

记录函数执行时间

测量函数执行时间

func longRunningTask() {
defer func(start time.Time) {
fmt.Printf("Execution time: %v\n", time.Since(start))
}(time.Now())

// 执行耗时任务...
}

修改返回值

在函数返回前修改命名返回值

func process(data string) (result string, err error) {
defer func() {
if err != nil {
result = "fallback value" // 修改返回值
}
}()

// 处理逻辑...
if somethingWrong {
return "", errors.New("error occurred")
}
return "processed", nil
}

高级用法

多个 defer 的执行顺序

多个 defer 按后进先出(LIFO)的顺序执行

func multipleDefers() {
defer fmt.Println("First defer")
defer fmt.Println("Second defer")
defer fmt.Println("Third defer")

fmt.Println("Function body")
}
/* 输出:
Function body
Third defer
Second defer
First defer
*/

参数即时求值

defer 的参数在声明时立即求值

func parameterEvaluation() {
i := 0
defer fmt.Println("Deferred value:", i) // 输出 0
i++
fmt.Println("Current value:", i) // 输出 1
}

闭包中的变量引用

使用闭包可以引用最新的变量值

func closureReference() {
i := 0
defer func() {
fmt.Println("Deferred closure:", i) // 输出 2
}()

i = 1
i = 2
}

循环中的defer直接捕获循环变量会导致引用同一地址:

for i := 0; i < 3; i++ {
defer func() { fmt.Println(i) }() // 输出3次3(错误)
// 正确:通过参数传递当前值
defer func(n int) { fmt.Println(n) }(i) // 输出2,1,0
}

注意事项与最佳实践

循环中的 defer

在循环中使用 defer 可能导致资源未及时释放

// 反模式 - 所有 defer 在函数结束时执行
func processFiles(filenames []string) {
for _, name := range filenames {
file, err := os.Open(name)
if err != nil {
log.Println(err)
continue
}
defer file.Close() // 所有文件在函数结束时才关闭
// 处理文件...
}
}

// 正确做法 - 使用函数包装
func processFilesCorrect(filenames []string) {
for _, name := range filenames {
func() {
file, err := os.Open(name)
if err != nil {
log.Println(err)
return
}
defer file.Close() // 每个文件在包装函数结束时关闭
// 处理文件...
}()
}
}

性能考虑

在性能敏感的代码中避免不必要的 defer

// 对于简单的锁操作,直接解锁可能比 defer 更高效
func criticalSection() {
mu.Lock()
// 非常简短的临界区
mu.Unlock() // 比 defer 更高效
}

错误处理

defer 中的错误常被忽略,需要特别处理

func writeFile(filename string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer func() {
if cerr := file.Close(); cerr != nil && err == nil {
err = cerr // 捕获关闭错误
}
}()

// 写入数据...
_, err = file.Write([]byte("data"))
return err
}

避免在 defer 中修改返回值

除非必要,避免在 defer 中修改返回值,这可能导致难以追踪的 bug

// 可能引起困惑的代码
func confusing() (x int) {
defer func() { x++ }()
return 1 // 实际返回 2
}

defer 与 panic/recover 的交互

理解 defer 在 panic 情况下的执行顺序

func panicExample() {
defer fmt.Println("First defer")
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
defer fmt.Println("Second defer")

panic("something went wrong")
}
/* 输出:
Second defer
Recovered: something went wrong
First defer
*/