应使用 errors.Is 和 errors.As 替代 == 与类型断言,以安全穿透多层错误包装;优先通过自定义错误类型封装语义化方法;用命名返回+defer简化错误处理流程;避免滥用 panic,仅用于不可恢复的程序缺陷。

用 errors.Is 和 errors.As 替代 == 和类型断言
直接用 err == io.EOF 或 if e, ok := err.(*os.PathError); ok 看似简单,但容易漏掉包装错误(比如被 fmt.Errorf("read failed: %w", err) 包裹后的 io.EOF)。Go 1.13 引入的 errors.Is 和 errors.As 能穿透多层 %w,安全判断错误本质。
-
errors.Is(err, io.EOF)—— 判断是否(直接或间接)等于某个目标错误 -
errors.As(err, &target)—— 尝试解包并赋值给目标变量,返回是否成功 - 注意:不要在循环里反复调用
errors.As做类型分支,应优先用接口方法抽象行为
把错误分类封装成自定义类型 + 方法
当一类错误需要统一处理逻辑(如重试、降级、日志脱敏),硬写一堆 if errors.Is(err, xxx) || errors.Is(err, yyy) 很难维护。更清晰的做法是定义错误类型,并附带语义化方法。
type ValidationError struct {
Field string
Value interface{}
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on field %s", e.Field)
}
func (e *ValidationError) IsRetryable() bool { return false }
func (e *ValidationError) ShouldLog() bool { return true }
- 业务函数返回
&ValidationError{Field: "email"},调用方用errors.As(err, &e)捕获后直接调e.ShouldLog() - 避免在错误值上放太多状态字段——它不是数据载体,而是控制流信号
- 不要实现
Unwrap()除非你明确要参与错误链传递
用 defer + named return 避免重复 return err
多个操作需按序执行、任一失败就终止并返回错误时,传统写法会堆叠大量 if err != nil { return err }。用命名返回参数配合 defer 可让主流程更线性。
func processFile(path string) (err error) {
f, err := os.Open(path)
if err != nil {
return
}
defer func() {
if closeErr := f.Close(); closeErr != nil && err == nil {
err = closeErr
}
}()
// 后续操作不再需要每个都写 if err != nil { return }
data, err := io.ReadAll(f)
if err != nil {
return
}
return json.Unmarshal(data, &result)
}
- 命名返回参数
err让defer内部能修改最终返回值 -
defer中检查err == nil是关键,否则会覆盖原始错误 - 仅适用于单个资源清理场景;多个资源建议用
multierr或显式分层 defer
别用 panic/recover 替代错误返回
除了真正不可恢复的程序缺陷(如 nil 指针解引用、切片越界),其他所有业务异常都应该走 error 返回路径。用 panic 处理可预期失败,会导致调用栈丢失、延迟不可控、无法被中间件统一拦截。
-
http.HandlerFunc中panic("not found")不如返回http.Error(w, "not found", http.StatusNotFound) - 数据库查询返回空结果?不是错误,应返回
nil或自定义零值,而非errors.New("no rows") - 第三方 SDK 抛 panic?用
recover捕获后立即转为error向上传递,不要继续 panic
return err 前,花两秒想清楚:这个 err 对调用方意味着什么?它该被重试、忽略、记录,还是立刻终止整个事务?










