Go错误处理核心是结构化语义:需用哨兵错误、自定义类型(含Unwrap)、%w包装、显式状态码映射,禁字符串匹配、裸panic及敏感信息泄露。

Go 框架中错误处理不是“加个 if err != nil 就完事”,关键在于错误能否被下游(调用方、日志系统、监控告警、前端)正确识别、分类、响应。鲁棒性差的错误处理,往往表现为 panic 频发、错误信息丢失、业务码和系统码混杂、无法做 errors.Is 判断。
error 值必须携带可判定的语义,不能只靠字符串匹配
很多框架在 HTTP handler 里直接返回 fmt.Errorf("user not found"),这会导致调用方无法区分是用户不存在、DB 连接失败,还是权限不足——三者都含 “not found”。真正的鲁棒性来自结构化错误标识。
- 定义哨兵错误(sentinel errors),如
var ErrUserNotFound = errors.New("user not found"),供errors.Is(err, ErrUserNotFound)精确判断 - 避免用
strings.Contains(err.Error(), "not found")做逻辑分支——字符串易变、大小写敏感、翻译后失效 - 自定义错误类型必须实现
Unwrap() error(若包装了底层错误),否则errors.Is和errors.As会失效
HTTP 框架中 error 转 HTTP 状态码必须显式映射,不能靠默认 fallback
像 Gin、Echo、Chi 这类框架常提供 c.AbortWithError(404, err),但它们**不会自动解析 err 的业务含义**。如果你传入一个未包装的 os.ErrNotExist,它大概率被转成 500,而不是 404。
- 统一错误中间件里,应基于错误类型/哨兵值做 switch 分支:
if errors.Is(err, ErrUserNotFound) { statusCode = 404 } - 不要依赖框架对
err.Error()的关键词嗅探(如某些旧版中间件尝试匹配 “not found” 字符串) - 对第三方库错误(如
pgx.ErrNoRows),用errors.As提取并映射,而非假设其字符串格式稳定
error 包装必须用 %w,且仅在增加新上下文时包装
fmt.Errorf("failed to parse config: %w", err) 是推荐写法;但滥用包装会让错误链过长、原始错误被埋没,甚至破坏 errors.Is 的穿透能力。
立即学习“go语言免费学习笔记(深入)”;
- 只在跨层传递时包装:DB 层 → Service 层 → Handler 层,每层加一句“做了什么”,不重复包装同一错误
- 禁止在同层多次包装:
fmt.Errorf("step A: %w", err)→fmt.Errorf("step B: %w", err)(此时 err 已含 “step A”,再包就冗余) - 日志记录前建议用
fmt.Sprintf("%+v", err)查看完整错误链,确认原始错误是否仍可被errors.Is触达
panic 仅用于真正不可恢复的初始化失败,业务逻辑中禁止裸 panic
框架启动时配置加载失败、端口被占用、gRPC server 注册失败,这些可以 panic;但用户提交非法 JSON、token 过期、库存不足,这些全是预期中的业务错误,必须走 error 返回路径。
- 所有 HTTP handler、gRPC method、定时任务函数,入口处加
defer func() { if r := recover(); r != nil { log.Panic(...) } }()是掩盖问题,不是防御 - goroutine 内部若需 recover,必须明确捕获后转为
error向上传递,例如:return fmt.Errorf("background job panicked: %v", r) - 使用
log.Fatal替代panic更清晰——它终止进程且带堆栈,语义更接近“服务无法启动”
最易被忽略的一点:错误的生命周期管理。一个被 fmt.Errorf("process order %d: %w", id, err) 包装过的错误,如果 id 是用户可控输入(比如 URL path 中的 /orders/{id}),就可能泄露敏感 ID 到日志或监控指标中——错误上下文要脱敏,不是加了 %w 就万事大吉。


















