前言
defer是Go语言中极具特色的关键字,用于注册延迟调用。当函数执行到defer语句时,不会立即执行被延迟的函数调用,而是将调用压入一个栈中,在函数即将返回时(LIFO顺序)执行。理解defer的执行时机和机制,对于写出健壮的Go代码至关重要。
一、defer基础
1.1 defer的基本用法
func before() {
fmt.Println("before main")
}
func after() {
fmt.Println("after main")
}
func main() {
defer after()
before()
fmt.Println("main body")
}
输出:
before main
main body
after main
1.2 defer的执行时机
defer在return语句之后、函数退出之前执行:
func test() int {
fmt.Println("1. 函数体执行")
ret := 0
defer func() {
fmt.Println("4. defer执行,ret被修改")
ret = 100
}()
fmt.Println("2. defer注册完毕,继续执行")
return ret // 3. return执行,ret=0
}
func main() {
result := test()
fmt.Printf("5. 最终返回值: %d\n", result) // 注意:返回值不是100
}
关键发现: defer修改的是命名返回值,但返回的是之前的值副本。
1.3 LIFO执行顺序
多个defer按后进先出顺序执行:
func main() {
fmt.Println("start")
defer fmt.Println("defer 1")
defer fmt.Println("defer 2")
defer fmt.Println("defer 3")
fmt.Println("middle")
defer fmt.Println("defer 4")
defer fmt.Println("defer 5")
fmt.Println("end")
}
输出:
start
middle
end
defer 5 ← LIFO:最后注册的先执行
defer 4
defer 3
defer 2
defer 1
二、defer与返回值
2.1 匿名返回值 vs 命名返回值
匿名返回值:
func匿名() int {
var result int
defer func() {
result = 100
fmt.Println("defer修改:", result)
}()
return result // 返回0(result的副本)
}
func main() {
fmt.Println("匿名返回值:", 匿名()) // 打印100
}
命名返回值:
func命名() (result int) {
defer func() {
result = 100
fmt.Println("defer修改:", result)
}()
return result // 返回100(与result是同一变量)
}
func main() {
fmt.Println("命名返回值:", 命名()) // 打印100
}
2.2 图解defer执行时机
return 执行过程:
return xxx
│
▼
┌──────────────────┐
│ 1. 计算返回值 │ ← 返回值已确定
├──────────────────┤
│ 2. 调用defer函数 │ ← defer在这里执行
├──────────────────┤
│ 3. 返回调用者 │
└──────────────────┘
注意:步骤1和步骤2之间,命名返回值已经被赋值
三、defer与panic
3.1 defer在panic时的执行
func main() {
fmt.Println("start")
defer fmt.Println("defer 1")
defer fmt.Println("defer 2")
defer fmt.Println("defer 3")
panic("something went wrong")
defer fmt.Println("never reached")
}
输出:
start
defer 3 ← panic前的defer倒序执行
defer 2
defer 1
panic: something went wrong
3.2 recover拦截panic
func safeCall(f func()) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("捕获panic: %v\n", r)
}
}()
f()
}
func mayPanic() {
fmt.Println("mayPanic 开始")
panic("boom!")
fmt.Println("mayPanic 结束") // 不会执行
}
func main() {
fmt.Println("main 开始")
safeCall(mayPanic)
fmt.Println("main 继续执行")
}
输出:
main 开始
mayPanic 开始
捕获panic: boom!
main 继续执行
3.3 defer中panic的传递
func main() {
defer func() {
fmt.Println("outer defer start")
defer func() {
if r := recover(); r != nil {
fmt.Printf("inner recover: %v\n", r)
}
}()
defer fmt.Println("inner defer")
panic("inner panic")
}()
panic("outer panic")
}
输出:
inner defer
inner recover: inner panic
outer defer start
分析:
-
outer panic触发
-
outer defer开始执行,输出 "outer defer start"
-
遇到 inner defer,注册
-
遇到 inner recover
-
遇到 inner panic(新的panic)
-
新的panic触发,inner recover捕获 "inner panic"
-
outer defer结束
四、defer参数求值时机
4.1 参数是立即求值的
func main() {
i := 0
defer fmt.Println("defer i =", i) // 参数立即求值,i=0
i = 100
fmt.Println("main i =", i) // i=100
}
输出:
main i = 100
defer i = 0 ← defer注册时i=0被保存
4.2 闭包捕获的是变量引用
func main() {
i := 0
defer func() {
fmt.Println("闭包 i =", i) // 闭包捕获i的引用
}()
i = 100
fmt.Println("main i =", i)
}
输出:
main i = 100
闭包 i = 100 ← defer执行时,i已经是100
4.3 对比分析
func compare() {
i := 0
// 方式1:参数求值
defer fmt.Println("参数方式:", i)
// 方式2:闭包方式
defer func() {
fmt.Println("闭包方式:", i)
}()
i = 100
}
func main() {
compare()
}
输出:
闭包方式: 100
参数方式: 0
五、defer的典型应用
5.1 资源释放
func readFile(filename string) {
// 打开文件
file, err := os.Open(filename)
if err != nil {
fmt.Printf("打开文件失败: %v\n", err)
return
}
// 确保关闭文件
defer file.Close()
// 读取文件内容
data := make([]byte, 1024)
for {
n, err := file.Read(data)
if n == 0 || err != nil {
break
}
fmt.Print(string(data[:n]))
}
// defer会在函数结束时自动关闭文件
}
5.2 解锁Mutex
import "sync"
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock() // 函数结束自动解锁
c.count++
}
func (c *Counter) Get() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
5.3 释放数据库连接
type DB struct {
conn interface{}
}
func query(db *DB, sql string) {
// 获取连接
conn := db.getConn()
defer db.releaseConn(conn) // 确保释放
// 使用连接执行查询
results := conn.Query(sql)
// 处理结果...
// defer自动释放连接
}
5.4 打印函数执行时间
func trackExecution(name string) {
start := time.Now()
fmt.Printf("开始执行 %s...\n", name)
defer func() {
elapsed := time.Since(start)
fmt.Printf("%s 执行耗时: %v\n", name, elapsed)
}()
// 模拟执行
time.Sleep(100 * time.Millisecond)
}
func main() {
trackExecution("task1")
trackExecution("task2")
}
5.5 统一错误处理
func process() (err error) {
// 使用命名返回值,确保defer能访问到err
defer func() {
if err != nil {
fmt.Printf("最终错误: %v\n", err)
}
}()
// 步骤1
if err = step1(); err != nil {
return fmt.Errorf("step1 failed: %w", err)
}
// 步骤2
if err = step2(); err != nil {
return fmt.Errorf("step2 failed: %w", err)
}
// 步骤3
if err = step3(); err != nil {
return fmt.Errorf("step3 failed: %w", err)
}
return nil
}
func step1() error { return nil }
func step2() error { return errors.New("step2 error") }
func step3() error { return nil }
六、defer的性能
6.1 defer的性能开销
defer比直接调用有一定的性能开销:
import (
"testing"
)
func withoutDefer() {
// 直接调用
}
func withDefer() {
defer func() {
// 空defer
}()
}
func BenchmarkWithoutDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
withoutDefer()
}
}
func BenchmarkWithDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
withDefer()
}
}
// 典型结果:
// BenchmarkWithoutDefer 1000000000 0.25 ns/op
// BenchmarkWithDefer 1000000000 35 ns/op
//
// defer大约有100倍的开销,但在大多数场景下可忽略
6.2 何时应该避免使用defer
// 场景:循环中大量创建资源
// 不推荐:每次循环都注册defer
func processItems(items []Item) {
for _, item := range items {
file, _ := os.Open(item.Path)
defer file.Close() // 问题:defer累积,资源无法及时释放
// 处理...
}
}
// 推荐:使用代码块限制作用域
func processItemsFixed(items []Item) {
for _, item := range items {
file, _ := os.Open(item.Path)
// 处理完后立即关闭,或使用errgroup批量处理
file.Close()
}
}
七、常见面试题
Q1: 下面代码的输出是什么?
func main() {
defer_call()
}
func defer_call() {
defer func() { fmt.Println("1") }()
defer func() { fmt.Println("2") }()
defer func() { fmt.Println("3") }()
panic("panic error")
}
答案:
3
2
1
panic: panic error
Q2: defer的值捕获问题
func main() {
var fs = make([]func(), 3)
for i := 0; i < 3; i++ {
fs[i] = func() {
fmt.Print(i, " ")
}
}
for _, f := range fs {
f()
}
}
答案: 2 2 2(闭包捕获循环变量i的引用)
修正:
for i := 0; i < 3; i++ {
v := i // 创建副本
fs[i] = func() {
fmt.Print(v, " ")
}
}
// 输出:0 1 2
Q3: defer在return之后的执行
func test() (i int) {
defer func() { i++ }()
return 5
}
func main() {
fmt.Println(test()) // 输出 6
}
答案: 6。return 5先将i设为5,然后defer执行i++,最终返回6。
总结
-
执行时机:defer在return之后、函数退出前执行
-
LIFO顺序:多个defer按后进先出执行
-
参数求值:defer语句的参数立即求值,但闭包按引用捕获
-
panic行为:panic触发时,defer仍会执行
-
recover:只在defer中调用才能捕获panic
-
性能:有轻微开销(约35ns),但对大多数应用可忽略
最佳实践:
-
总是使用defer释放资源(文件、连接、锁)
-
在循环中谨慎使用defer
-
defer参数立即求值,闭包按引用捕获
-
命名返回值+defer可以实现灵活的清理逻辑
💡 下一篇文章我们将深入讲解Go语言的接口与nil,敬请期待!