go中defer从入门到进阶

发布于:2025-04-03 ⋅ 阅读:(16) ⋅ 点赞:(0)

defer在开发中的应用

Go 语言中的 defer:从入门到精通

引言

如果你刚接触 Go 语言,第一次看到 defer 关键字,可能会有点疑惑:

func main() {
    defer fmt.Println("Hello")
    fmt.Println("World")
}

运行后输出:

World
Hello

乍一看,defer 似乎在“搞延迟执行的把戏”,但它的作用远不止如此。在实际开发中,defer 可以帮我们实现资源释放、错误处理等功能,写出更优雅的代码。

今天,我们就来全面剖析 defer,带你从入门到精通。


1. defer 的基础用法

defer 语句用于 延迟执行 一个函数,直到所在的函数即将返回时再执行。

1.1 最基本的使用方式

func main() {
    defer fmt.Println("执行了 defer")
    fmt.Println("函数执行中...")
}

输出:

函数执行中...
执行了 defer

可以看到,defer 语句会在 函数返回前 执行。

1.2 defer 的执行顺序(LIFO 规则)

如果有多个 defer,它们会 按照后进先出(LIFO, Last In First Out) 的顺序执行:

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

输出:

3
2
1

也就是说,最后 defer 的语句最先执行,类似于栈的结构

1.3 defer 绑定的参数

defer 语句在声明时就会对参数进行求值,而不是等到真正执行时才计算。

func main() {
    x := 10
    defer fmt.Println("defer x:", x)
    x = 20
    fmt.Println("main x:", x)
}

输出:

main x: 20
defer x: 10

defer 在声明时就“捕获”了 x 的值(10),即使后续 x 变成了 20,defer 执行时仍然使用 10。

如果 defer 需要使用“最终的值”,可以传入 指针闭包

func main() {
    x := 10
    defer func() { fmt.Println("defer x:", x) }()
    x = 20
    fmt.Println("main x:", x)
}

输出:

main x: 20
defer x: 20

由于 defer 绑定的是 闭包,所以 x 的最终值是 20。


2. defer 的应用场景

2.1 资源释放

在处理文件、数据库连接等资源时,defer 让代码更简洁,避免忘记释放资源。

func readFile(filename string) {
    file, err := os.Open(filename)
    if err != nil {
        fmt.Println("打开文件失败:", err)
        return
    }
    defer file.Close() // 确保文件在函数退出时关闭
    fmt.Println("读取文件...")
}

2.2 互斥锁解锁

var mu sync.Mutex

func criticalSection() {
    mu.Lock()
    defer mu.Unlock()
    fmt.Println("执行关键代码区域")
}

这样,无论函数中途如何返回,互斥锁都会被正确释放,避免死锁。

2.3 计算执行时间(简单性能分析)

func trackTime() func() {
    start := time.Now()
    return func() {
        fmt.Println("执行时间:", time.Since(start))
    }
}

func main() {
    defer trackTime()()
    time.Sleep(2 * time.Second)
}

2.4 defer 与 panic/recover 的配合

deferpanic 发生时仍然会执行,因此常用于 异常捕获,避免程序崩溃。

func protect() {
    if r := recover(); r != nil {
        fmt.Println("捕获 panic:", r)
    }
}

func mayPanic() {
    defer protect() // 保护代码块
    panic("发生严重错误!") // 触发 panic
}

func main() {
    mayPanic()
    fmt.Println("程序继续运行")
}
  1. recover() 只有在 defer 作用域内才能捕获 panic,否则 panic 仍然会导致程序崩溃。
  2. 多个 defer 仍然遵循 LIFO 规则,可以在多个 defer 里做不同的善后处理。

总结使用场景

场景 示例
释放锁 defer mu.Unlock()
关闭文件 defer file.Close()
关闭网络连接 defer conn.Close()
关闭数据库连接 defer db.Close()
捕获 panic defer recover()
计算执行时间 defer time.Since(start)
多个 defer 逆序执行 defer fmt.Println()
删除临时文件 defer os.Remove()

defer进阶以及注意点

第一眼看到defer其实就是认为是一个延迟执行,但是在一些复杂情况下,defer 的行为可能并不像你想的那么直观,甚至可能导致 隐藏的 bug性能问题;再总结一下defer中可能会犯的错误。


1. defer 与 panic/recover

deferpanic 发生时仍然会执行,这意味着它可以用来拦截异常,防止程序直接崩溃。

func protect() {
    if r := recover(); r != nil {
        fmt.Println("捕获 panic:", r)
    }
}

func mayPanic() {
    defer protect()
    panic("发生严重错误!")
}

func main() {
    mayPanic()
    fmt.Println("程序继续运行")
}

关键点

  1. recover() 只有在 defer 作用域内才能捕获 panic,否则 panic 仍然会导致程序崩溃。
  2. 多个 defer 仍然遵循 LIFO 规则,可用于分层处理不同的异常情况。

2. defer 影响返回值的两种情况

情况 1:普通返回值不会被 defer 修改

func test() int {
    x := 10
    defer func() {
        x = 20
    }()
    return x
}

func main() {
    fmt.Println(test()) // 输出?
}

输出:10(因为 return x 先执行defer 修改 x 但不会影响返回值)


情况 2:命名返回值可以被 defer 修改

func test() (x int) {
    x = 10
    defer func() {
        x = 20
    }()
    return
}

func main() {
    fmt.Println(test()) // 输出?
}

输出:20(因为 x命名返回值defer 直接修改 x,最终返回 20)


3. defer 在循环中的性能问题

错误示范

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

这会让 defer 在内存中积累 100 万个调用,导致 严重的性能问题

优化方案

func goodLoop() {
    for i := 0; i < 1000000; i++ {
        fmt.Println(i) // 直接执行,避免不必要的 defer
    }
}

适用场景

  • 仅在必要时使用 defer,比如 文件、数据库连接的释放

4. defer 与接口方法的坑

示例

type User struct {
    name string
}

func (u *User) Print() {
    fmt.Println("User:", u.name)
}

func main() {
    u := &User{name: "Alice"}
    defer u.Print()
    u.name = "Bob"
}

输出:Bob(因为 defer 绑定的是 u.Print(),而 u.namedefer 执行前已被修改)

如何绑定 defer 时的变量值?

defer func(name string) {
    fmt.Println("User:", name)
}(u.name) // 传入当前 name 值

这样 defer 绑定的就是 "Alice",不会受后续修改影响。


5. defer 关闭 channel 需要谨慎

错误示范

func main() {
    ch := make(chan int)
    defer close(ch) // ⚠️ 可能 panic
    ch <- 1
}

正确方式:让 唯一的发送方 负责关闭 channel

func sender(ch chan int) {
    defer close(ch)
    ch <- 1
}

func main() {
    ch := make(chan int)
    go sender(ch)
    fmt.Println(<-ch)
}

6. os.Exit(0) 会跳过所有 defer

错误示范

func main() {
    defer fmt.Println("这条语句不会执行")
    os.Exit(0) // 直接终止程序
}

正确方式

func main() {
    defer fmt.Println("程序即将退出")
    return // 让函数正常返回
}

总结

defer 使用中的注意要点,防止误用和错用:

  1. LIFO 执行顺序,最后声明的 defer 先执行。
  2. 参数绑定时机:普通参数在 defer 声明时绑定,闭包/指针 绑定最终值。
  3. defer 可配合 panic/recover 进行异常捕获,防止程序崩溃。
  4. 循环中慎用 defer,避免创建大量 defer,影响性能。
  5. 返回值可能会被 defer 修改,特别是命名返回值。
  6. defer 关闭 channel 需要谨慎,避免多次关闭导致 panic。
  7. os.Exit(0) 直接终止程序,跳过所有 defer