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") 是脆弱的——底层错误实现一变,逻辑就崩;而且无法跨包装层级工作。
立即学习“go语言免费学习笔记(深入)”;
- ✅ 安全判断:
if errors.Is(err, os.ErrNotExist) { ... }—— 穿透任意层%w包装 - ✅ 提取上下文:
var pathErr *os.PathError; if errors.As(err, &pathErr) { log.Printf("failed on path: %s", pathErr.Path) } - ⚠️ 常见坑:在中间件里统一
http.Error(w, err.Error(), 500),把本该是 400 的校验错误也打成 500,掩盖真实语义
对外返回用户消息时,必须脱敏+本地化+格式统一
err.Error() 是给开发者看的,不是给用户看的。它可能包含路径、SQL 片段、系统调用名等敏感或无用信息。
- ✅ 正确流程:handler 内用
errors.As匹配已知错误类型 → 调用其UserMessage()方法 → 返回标准 JSON:{"code": 404, "message": "用户不存在"} - ❌ 危险操作:直接
json.NewEncoder(w).Encode(map[string]any{"error": err.Error()}) - ? 记住:日志里要留完整错误链(含栈、cause、context),响应体里只放脱敏后结果;开发环境也不该暴露数据库错误码
最常被忽略的一点:错误包装不是越多越好。A → B → C 都用 %w 没问题,但 A → B(%w) → C(%v) 就断了——整条链只剩最后一层可见。调试时你看到的“failed to serve HTTP”底下,可能埋着一个被吞掉的 permission denied,而你完全不知道。










