panic/recover 开销远高于普通错误返回,因需栈展开和状态记录,吞吐量可降100倍以上;error接口返回仅指针传递,几乎无成本;defer单次开销纳秒级,但高频滥用会影响性能。
Go 里用 error 接口返回错误(比如 os.Open 返回 (*File, error))几乎无运行时成本,只是指针传递和接口赋值。但一旦触发 panic,运行时需展开栈、记录 goroutine 状态、构造调用链,开销是数量级差异。
实测在循环中每轮都 panic 再 recover,吞吐量可能下降 100 倍以上;而正常 if err != nil 判断基本不影响性能。
panic 捕获空指针然后 recover,看似“兜底”,实际把可控错误变成了高开销路径BenchmarkFoo-8 中混入 recover 会严重污染结果,建议单独测 panic 路径defer 不是免费的:每次执行都会在当前 goroutine 的 defer 链表上插入一个节点,runtime 需要管理这些延迟调用。但单次 defer 开销极小(纳秒级),真正要注意的是高频小函数里滥用 defer。
func bad() error {
f, err := os.Open("x")
if err != nil {
return err
}
defer f.Close() // 这里没问题
for i := 0; i < 10000; i++ {
defer fmt.Println(i) // 危险:1 万个 defer 节点,内存+调度开销明显
}
return nil
}
errors.Is 和 errors.As 需要遍历错误链(通过 Unwrap()),最坏情况是 O(n) 时间复杂度。不过绝大多数业务错误链很短(1–3 层),实际开销可以忽略。
真正要注意的是:别在 QPS 上万的请求内循环调用它们做条件判断。
os.ErrNotExist,直接用 err == os.ErrNotExist 更快(前提是没被 fmt.Errorf 包裹)Is(error) bool 方法可跳过默认遍历逻辑errors.Unwrap(err) 单次调用成本低,但反复 for err != nil { err = errors.Unwrap(err) } 不如用 errors.Is
fmt.Errorf("failed to %s: %w", op, err) 会触发字符串格式化和堆分配,比直接返回原 err 多一次内存分配。但在非高频路径(如初始化、配置加载)里,这点开销无关紧要。
真正在意性能时,可考虑预分配或错误池,但绝大多数服务无需为此优化。
fmt.Errorf("http %d: %w", status, err) —— 直接传原始 error 更
轻量var ErrNotFound = errors.New("not found"),零分配errors.Join(Go 1.20+)合并多个 error 会产生新对象,但仅当需要同时暴露多个原因时才用fmt.Sprint。