正确做法是用匿名函数传参捕获时间点:defer func(t time.Time) { ... }(time.Now());多个 defer 应按作用域分层封装,避免嵌套混乱;高频小函数宜手动计时以避 defer 开销;recover 与耗时统计需拆为两个 defer 以确保顺序正确。

Defer 里怎么拿到函数执行耗时
不能直接在 defer 里用 time.Now() 算差值,因为 defer 是后进先出,且闭包捕获的是变量引用——如果函数里改了开始时间变量,defer 执行时看到的就是最后的值。
正确做法是让 defer 捕获“那一刻”的值,用匿名函数传参:
func example() {
start := time.Now()
defer func(t time.Time) {
fmt.Printf("took %v\n", time.Since(t))
}(start)
// ... 业务逻辑
}
- 必须把
start当参数传进匿名函数,否则闭包会共享变量 - 别写成
defer func() { ... }(time.Now())—— 这样time.Now()在defer注册时就执行了,不是函数退出时的时间 - 如果函数可能 panic,
defer仍会执行,所以这个模式天然支持异常路径耗时统计
多个 defer 怎么避免嵌套混乱
一个函数里放七八个 defer 统计不同阶段,容易错乱顺序、漏掉恢复或重复打印。关键不是“加多少”,而是“谁该管哪段”。
推荐按作用域分层,用局部作用域封装计时逻辑:
立即学习“go语言免费学习笔记(深入)”;
func handleRequest() {
defer trace("handleRequest")()
// ...
dbQuery := trace("db.Query")
defer dbQuery()
rows, _ := db.Query(...)
// ...
}
func trace(name string) func() {
start := time.Now()
return func() {
log.Printf("%s: %v", name, time.Since(start))
}
}
-
trace返回一个闭包,自带独立的start,互不干扰 - 每个
defer只负责一件事,比如只打日志、只发 metric、只 recover panic - 避免在
defer里调用可能阻塞或失败的函数(如网络写入),否则拖慢函数退出
为什么 defer 不适合高频小函数的性能统计
每次 defer 注册都有运行时开销:要分配 defer 记录结构、维护 defer 链表、panic 时还要遍历。压测下百万次调用,defer 本身能占到总耗时 5%~10%。
- 短平快函数(比如简单计算、map 查找)建议直接用
time.Now()+ 手动记录,省掉 defer 调度成本 - Go 1.22+ 对 defer 做了优化,但仅限于“无 panic 场景下的简单 defer”,带闭包或参数传递的仍走老路径
- 如果真要 AOP 式统统计,更稳的方式是代码生成(go:generate)或 eBPF 工具(如 bpftrace),而不是 runtime 层面堆 defer
recover 和耗时统计怎么共存不打架
想在 defer 里既 recover panic,又统计耗时,但 recover() 必须在 defer 函数里调用才有效——而一旦你把耗时统计和 recover 写进同一个匿名函数,panic 后的耗时就变成“从 panic 发生到 defer 执行完”的时间,不是原函数真实执行时间。
- 拆成两个
defer:先注册 recover 的,再注册耗时的;Go 保证它们按注册逆序执行,所以 recover 先跑,不影响耗时逻辑 - recover 的 defer 里别做重操作(如打印大 struct、调远程服务),否则会污染“函数退出耗时”这个指标
- 如果函数里手动调了
os.Exit()或发生 runtime crash,defer根本不执行,这类情况耗时自然统计不到
真正难的不是写出来,是怎么让每个 defer 都知道自己该在哪一刻“定格”,又不干扰其他逻辑。稍不注意,时间就记歪了,panic 也 recover 不住。











