go 中 panic 仅用于不可恢复的编程错误,常规错误必须返回 error;应避免在业务逻辑中使用 recover,context 取消信号不应误作 error 处理。

panic 是错误处理的逃生舱,不是常规出口
Go 的 panic 不是 Java 的 Exception,也不是 Rust 的 panic!(后者在 debug 模式下才触发)。它本质是程序级崩溃信号,会立即终止当前 goroutine 的执行,并开始栈展开——这意味着所有 defer 都会运行,但无法被上层“捕获并恢复”为业务逻辑流。除非你真在写 init 函数、测试断言、或极少数必须中止整个服务的临界错误(比如配置加载失败且无默认值),否则用 panic 处理可预期的错误,就是在把错误控制权交给 runtime。
常见错误现象:panic: assignment to entry in nil map 这类错误本该在初始化时检查,而不是靠 panic 暴露;又或者 HTTP handler 里对用户输入做 JSON 解析失败,直接 panic 导致整个请求挂掉,而不是返回 400 Bad Request。
实操建议:
- 把
panic限制在包内部不可恢复的编程错误场景:如传入空指针、违反接口契约、调用未实现的方法 - 对外暴露的函数/方法一律返回
error,哪怕底层用了panic(比如某些 parser 库),也要在外层 recover 并转成 error - 禁用
recover在 HTTP handler 或 RPC 方法中“兜底”——它掩盖了设计缺陷,且无法保证资源清理(如数据库连接、文件句柄)
error 类型不是占位符,要能区分语义和传播路径
很多 Go 项目里满屏 return errors.New("something went wrong"),或者更糟:直接 return nil, err 却不包装上下文。这导致调用方无法判断错误类型,也无法做针对性重试、降级或日志分级。
立即学习“go语言免费学习笔记(深入)”;
使用场景:API 调用失败要区分是网络超时(可重试)、认证失败(需刷新 token)、还是服务端 500(应告警);数据库操作失败要区分是约束冲突(UniqueViolation)、连接中断(sql.ErrConnDone)还是语法错误(开发期 bug)。
实操建议:
- 用
errors.Is(err, target)和errors.As(err, &target)替代字符串匹配;自定义 error 实现Is或Unwrap方法 - 用
fmt.Errorf("failed to parse config: %w", err)包装错误,保留原始 error 链;避免sprintf拼接丢失底层 error - 关键路径(如支付、库存扣减)的 error 必须带 trace ID 和时间戳,通过
errors.Join或自定义结构体携带元信息
defer + recover 只在极少数边界场景合法
recover 唯一合理用途是:在已知会 panic 的第三方库调用前后,做资源清理或转换错误类型。例如解析不受控的 Lua 脚本、调用 C 代码封装的库、或单元测试中验证 panic 行为。除此之外,在业务逻辑里加 defer func(){ if r := recover(); r != nil { ... } }(),等于主动放弃 Go 的错误显式传递哲学。
性能影响:每次 defer 都有少量开销;recover 本身不慢,但栈展开(panic 发生时)代价极高,比正常 error 返回慢 1–2 个数量级。
实操建议:
- 永远不要在 http.HandlerFunc 中用 recover 捕获 panic 后返回 500——应该用中间件统一处理,且只记录 panic 日志,不尝试“恢复”请求
- 如果必须 recover,确保只在最外层 goroutine(如 go http.Serve)做一次,且 recover 后立即 return,不再继续执行任何业务逻辑
- 测试中用
assert.Panics或testify/assert验证 panic 是 OK 的,但生产代码里不该出现 recover
ctx.Done() 不是 error,但常被误当错误处理
很多人写 if ctx.Err() != nil { return ctx.Err() } 然后一路往上 return,看起来很“Go 风格”,但忽略了:context 取消是协作式信号,不是错误。它不表示操作失败,而表示“调用方不关心结果了”。强行把它当 error 返回,会导致上游误判为系统异常,触发不必要的重试或告警。
兼容性影响:gRPC 客户端收到 CANCELLED 状态码时,默认不会重试;但如果你把 ctx.Err() 包装成自定义 ErrTimeout,就可能破坏这个语义。
实操建议:
- 在 I/O 操作前检查
ctx.Err(),及时退出;但不要把它塞进 error 返回值,尤其不要用%w包装进业务 error 链 - 数据库查询、HTTP 调用等阻塞操作,优先用支持 context 的版本(如
db.QueryContext、http.Client.Do),让底层自动响应 cancel - 日志中记录
ctx.Err()时,明确打标为 “canceled by caller”,而非 “failed with error”
真正难的不是写对一个 error 返回,而是整条调用链上每个环节都保持错误语义一致——从 HTTP 入口、到 service 层、再到 DAO,错误要么被处理,要么被明确透传,绝不静默吞掉,也绝不擅自升级成 panic。这点一旦松动,半年后查线上问题时,你会在日志里看到十层嵌套的 “unknown error”。











