Go中应使用fmt.Errorf(“%w”)包装错误以保留原始类型,支持errors.Is/As穿透判断;仅需添加上下文时包装,透传则直接return err;自定义错误需导出字段且有实际行为差异;对外响应须脱敏并统一格式。
Go 里返回有意义的错误,不是写得更长,而是让错误能被程序识别、能带上下文、还能安全展开——否则日志里全是“failed to do X”,线上出问题时只能靠猜。
用 fmt.Errorf + %w 包装,别用 %v 或字符串拼接
原始错误类型一旦丢失,errors.Is 和 errors.As 就失效,上层没法判断是不是 os.ErrNotExist 或 sql.ErrNoRows。
- ❌ 错误示范:
return fmt.Errorf("load config failed: %v", err)—— 断链,errors.Is(err, os.ErrNotExist)永远返回false - ✅ 正确做法:
return fmt.Errorf("loading config from %s failed: %w", path, err)—— 保留原始错误,支持穿透判断 - ⚠️ 注意:只在需要添加上下文的地方包装;如果只是透传,直接
return err更干净
什么时候该定义自定义错误类型
不是所有错误都值得定义结构体。只有当你需要携带额外字段(比如状态码、字段名、重试建议),或上层必须做精确行为分支时,才值得定义。
- ✅ 合理场景:
*ValidationError(含Field字段)、*APIError(含StatusCode和UserMessage) - ❌ 过度设计:
type ErrUserSaveFailed struct{}只实现空的Error()方法,和fmt.Errorf("save user failed: %w", err)没区别 - ? 关键细节:字段必须导出(如
Code int而非code int),否则errors.As提取失败
在 handler 中用 errors.Is 和 errors.As 替代字符串匹配
写 strings.Contains(err.Error(), "no such file") 是脆弱的——底层错误实现一变,逻辑就崩;而且无法跨包装层级工作。