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 的配合
defer
在 panic
发生时仍然会执行,因此常用于 异常捕获,避免程序崩溃。
go
func protect() {
if r := recover(); r != nil {
fmt.Println("捕获 panic:", r)
}
}
func mayPanic() {
defer protect() // 保护代码块
panic("发生严重错误!") // 触发 panic
}
func main() {
mayPanic()
fmt.Println("程序继续运行")
}
recover()
只有在defer
作用域内才能捕获panic
,否则panic
仍然会导致程序崩溃。- 多个
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
defer
在 panic
发生时仍然会执行,这意味着它可以用来拦截异常,防止程序直接崩溃。
go
func protect() {
if r := recover(); r != nil {
fmt.Println("捕获 panic:", r)
}
}
func mayPanic() {
defer protect()
panic("发生严重错误!")
}
func main() {
mayPanic()
fmt.Println("程序继续运行")
}
关键点
recover()
只有在defer
作用域内才能捕获panic
,否则panic
仍然会导致程序崩溃。- 多个
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.name
在 defer
执行前已被修改)
如何绑定 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
使用中的注意要点,防止误用和错用:
- LIFO 执行顺序 ,最后声明的
defer
先执行。 - 参数绑定时机 :普通参数在
defer
声明时绑定,闭包/指针 绑定最终值。 defer
可配合panic/recover
进行异常捕获,防止程序崩溃。- 循环中慎用
defer
,避免创建大量defer
,影响性能。 - 返回值可能会被
defer
修改,特别是命名返回值。 - defer 关闭
channel
需要谨慎,避免多次关闭导致 panic。 os.Exit(0)
直接终止程序,跳过所有defer
。