go错误处理强调显式传递与上下文包装:必须检查Error,用fmt.Errorf %w包装、errors.Join合并并发错误,自定义类型适用于需结构化判断的场景。

错误传递必须显式处理,Go 没有隐式异常传播
Go 的设计哲学是“错误即值”,error 是接口类型,不是异常。函数返回 error 后,调用方**必须检查并决定如何处理**——不检查、不返回、不包装,错误就丢了。常见现象是:深层调用出错,但上层日志只显示 "operation failed",无堆栈、无上下文、无法定位。
- 不要用
if err != nil { return err }一招鲜走天下,尤其在需要补充上下文时 - 避免在中间层吞掉错误(如
if err != nil { log.Println(err); return nil }),这等于主动放弃调试线索 - 不要用
panic替代错误传递,除非是真正的不可恢复程序崩溃(如配置解析失败且无法降级)
用 fmt.Errorf + %w 实现带链路的错误包装
从 Go 1.13 开始,fmt.Errorf 支持 %w 动词,用于包裹底层错误,形成可追溯的错误链。这是目前最轻量、标准库原生支持的传播方式。
func readFile(path string) error { data, err := os.ReadFile(path) if err != nil { return fmt.Errorf("failed to read config file %q: %w", path, err) } // ... return nil }
-
%w只能出现在格式字符串末尾,且仅接受一个error类型参数 - 被
%w包裹的错误可通过errors.Unwrap或errors.Is/errors.As向下提取 - 多次
%w包装会形成链,但不会自动记录调用位置;需配合runtime.Caller或第三方库补全栈信息
用 errors.Join 合并多个并发错误
当多个 goroutine 并行执行可能出错的操作(如批量写入多个服务),需要把所有错误聚合成一个统一返回值时,errors.Join 是 Go 1.20+ 提供的标准方案。
var errs []error for _, svc := range services { go func(s string) { if err := callService(s); err != nil { errs = append(errs, fmt.Errorf("service %q failed: %w", s, err)) } }(svc) } // 等待全部完成... if len(errs) > 0 { return errors.Join(errs...) }
-
errors.Join返回的错误实现了Unwrap() []error,可用errors.Unwrap拆解 - 它不改变原有错误语义,也不添加额外上下文,适合“汇总”而非“修饰”场景
- 注意竞态:
errs切片需加锁或改用sync.WaitGroup+ channel 收集,否则并发写入会 panic
自定义错误类型适合需结构化判断的场景
当错误需要携带状态码、重试标记、http 状态映射等元信息时,仅靠字符串包装不够用,应定义实现 error 接口的结构体。
立即学习“go语言免费学习笔记(深入)”;
type appError struct { Code int Message string Retry bool Err error } func (e *AppError) Error() string { return e.Message } func (e *AppError) Unwrap() error { return e.Err }
- 务必实现
Unwrap方法,否则errors.Is/errors.As无法穿透到原始错误 - 若需支持 jsON 序列化(如日志上报),可额外实现
Marshaljson - 避免过度设计:80% 的错误传播用
fmt.Errorf+%w足够,只有明确需要类型断言或行为区分时才引入自定义类型
错误传递最难的不是语法,而是决策点:什么时候该加前缀,什么时候该保留原始错误,什么时候该降级为 warning,什么时候该熔断。这些没有银弹,得结合业务 SLA、可观测性基建和团队 debug 习惯一起定。