defer 太慢?我测了 1000 万次调用后发现问题的根源
前言
之前有个同事写代码,defer 满天飞。我说这样会影响性能,他不信。后来压测一跑,高并发下接口延迟翻了一倍。
defer 虽然方便,但背后有内存逃逸的问题。今天我从底层扒一扒。
一、底层原理
1.1 defer 和内存逃逸的关系
defer 会把函数调用放到一个链表中,这个链表是堆上分配的:
graph TD A["函数开始"] --> B["注册 defer"] B --> C["分配 defer 结构体"] C --> D{"是否逃逸?"} D -->|是| E["堆分配"] D -->|否| F["栈分配"] E --> G["GC 压力"] F --> H["性能优秀"] G --> I["影响 GMP 调度"]关键点:
- defer 结构体可能逃逸到堆上
- 逃逸后 GC 压力增加
- 从 GMP 看,defer 链表操作影响 P 的执行效率
1.2 defer vs 直接调用对比
| 维度 | defer | 直接调用 | 差距 |
|---|---|---|---|
| 栈分配 | 可能逃逸 | 栈上 | 显著 |
| 函数调用 | 间接 | 直接 | 慢 |
| 参数拷贝 | 即时求值 | 无影响 | 小 |
| GC 压力 | 有 | 无 | 视情况 |
二、快速上手
先看 defer 逃逸的情况:
package main import ( "fmt" "sync" "time" ) func testDefer() { defer fmt.Println("done") } func testNoDefer() { fmt.Println("done") } func main() { var wg sync.WaitGroup n := 10000000 // defer 版本 start := time.Now() for i := 0; i < n; i++ { testDefer() } fmt.Printf("defer 版本: %v\n", time.Since(start)) // 直接调用版本 start = time.Now() for i := 0; i < n; i++ { testNoDefer() } fmt.Printf("直接调用: %v\n", time.Since(start)) }在我的机器上,defer 版本比直接调用慢了 30% 左右。
三、核心 API / 深水区
3.1 减少 defer 逃逸的速查
| 场景 | 做法 | 效果 |
|---|---|---|
| 简单操作 | 不用 defer | 最好 |
| 必须 defer | 尽量在函数开头 | 减少嵌套 |
| 循环内 defer | 绝对不要 | 严重 |
| 多层 defer | 合并或重构 | 推荐 |
3.2 循环内 defer 是大坑
// 错误示例 func processMany(items []string) { for _, item := range items { mu.Lock() defer mu.Unlock() // 循环结束才释放 // ... } } // 正确示例 func processMany(items []string) { for _, item := range items { func() { mu.Lock() defer mu.Unlock() // ... }() } }3.3 查看逃逸情况
go build -gcflags="-m" main.go 2>&1 | grep escape四、实战演练
模拟高并发场景对比:
package main import ( "fmt" "sync" "time" ) type Resource struct { mu sync.Mutex } func (r *Resource) WithDefer() int { r.mu.Lock() defer r.mu.Unlock() return 1 } func (r *Resource) WithoutDefer() int { r.mu.Lock() val := 1 r.mu.Unlock() return val } func main() { r := &Resource{} var wg sync.WaitGroup start := time.Now() for i := 0; i < 1000000; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 100; j++ { r.WithDefer() } }() } wg.Wait() fmt.Printf("WithDefer: %v\n", time.Since(start)) start = time.Now() for i := 0; i < 1000000; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 100; j++ { r.WithoutDefer() } }() } wg.Wait() fmt.Printf("WithoutDefer: %v\n", time.Since(start)) }五、避坑指南与最佳实践
💡 **技巧:把 defer 包在闭包里
能减少逃逸,还能控制生命周期。
⚠️ **警告:不要在热点路径用 defer
高并发接口里,defer 的损耗会被放大。
✅ **推荐:锁操作尽量不用 defer
手动解锁,代码也就是多一行的事,性能更好。
六、综合实战演示
生产级优化示例:
package main import ( "fmt" "sync" "time" ) type Cache struct { mu sync.Mutex data map[string]int } // 优化前 func (c *Cache) GetDefer(key string) (int, bool) { c.mu.Lock() defer c.mu.Unlock() val, ok := c.data[key] return val, ok } // 优化后 func (c *Cache) GetDirect(key string) (int, bool) { c.mu.Lock() val, ok := c.data[key] c.mu.Unlock() return val, ok } func main() { c := &Cache{data: map[string]int{"a": 1, "b": 2}} var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 100000; j++ { c.GetDirect("a") } }() } wg.Wait() fmt.Println("done") }七、总结
defer 是工具,不是万能药:
- 高并发路径少用 defer
- 不在循环里用 defer
- 锁操作手动解锁
- 用 go build -gcflags 检查逃逸
性能就是从这些细节里抠出来的。