go中defer从入门到进阶

defer在开发中的应用

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

引言

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

go 复制代码
func main() {
    defer fmt.Println("Hello")
    fmt.Println("World")
}

运行后输出:

复制代码
World
Hello

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

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


1. defer 的基础用法

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

1.1 最基本的使用方式

go 复制代码
func main() {
    defer fmt.Println("执行了 defer")
    fmt.Println("函数执行中...")
}

输出:

复制代码
函数执行中...
执行了 defer

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

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

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

go 复制代码
func main() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
}

输出:

复制代码
3
2
1

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

1.3 defer 绑定的参数

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

go 复制代码
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 需要使用"最终的值",可以传入 指针闭包

go 复制代码
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 让代码更简洁,避免忘记释放资源。

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

2.2 互斥锁解锁

go 复制代码
var mu sync.Mutex

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

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

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

go 复制代码
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 发生时仍然会执行,因此常用于 异常捕获,避免程序崩溃。

go 复制代码
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 发生时仍然会执行,这意味着它可以用来拦截异常,防止程序直接崩溃。

go 复制代码
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 修改

go 复制代码
func test() int {
    x := 10
    defer func() {
        x = 20
    }()
    return x
}

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

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


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

go 复制代码
func test() (x int) {
    x = 10
    defer func() {
        x = 20
    }()
    return
}

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

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


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

错误示范

go 复制代码
func badLoop() {
    for i := 0; i < 1000000; i++ {
        defer fmt.Println(i)
    }
}

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

优化方案

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

适用场景

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

4. defer 与接口方法的坑

示例

go 复制代码
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 时的变量值?

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

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


5. defer 关闭 channel 需要谨慎

错误示范

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

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

go 复制代码
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

错误示范

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

正确方式

go 复制代码
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

相关推荐
小吴先生66619 分钟前
Groovy 规则执行器,加载到缓存
java·开发语言·缓存·groovy
小杨40424 分钟前
springboot框架项目实践应用十四(扩展sentinel错误提示)
spring boot·后端·spring cloud
陈大爷(有低保)30 分钟前
Spring中都用到了哪些设计模式
java·后端·spring
秋风&萧瑟34 分钟前
【QT】QT的多界面跳转以及界面之间传递参数
开发语言·qt
程序员 小柴36 分钟前
SpringCloud概述
后端·spring·spring cloud
骑牛小道士37 分钟前
JAVA- 锁机制介绍 进程锁
java·开发语言
郭涤生40 分钟前
Chapter 1: Historical Context_《C++20Get the details》_notes
开发语言·c++20
独好紫罗兰1 小时前
洛谷题单2-P5712 【深基3.例4】Apples-python-流程图重构
开发语言·python·算法
喝醉的小喵1 小时前
分布式环境下的主从数据同步
分布式·后端·mysql·etcd·共识算法·主从复制
东方佑1 小时前
深度解析Python-PPTX库:逐层解析PPT内容与实战技巧
开发语言·python·powerpoint