17370845950

如何在Golang中返回用户友好的错误信息_Golang错误对外展示策略
错误信息不应直接返回原始error字符串,须脱敏、本地化并统一响应格式;应定义可识别的自定义错误类型,通过errors.As匹配并在handler中分别处理日志与用户提示。

错误信息不该直接返回 error.Error() 的原始字符串

Go 标准库和多数第三方包返回的 error(比如 os.Open 失败时的 "open /tmp/xxx: no such file or directory")包含路径、系统

调用名、底层细节,直接透出给终端用户既不安全也不友好。生产环境必须拦截、转换、脱敏。

  • 路径泄露可能暴露服务部署结构(如 /app/internal/config.yaml
  • 英文错误对中文用户不友好,且无法做 i18n 扩展
  • 不同错误类型需统一响应格式(如 HTTP 接口返回 {"code": 400, "message": "文件上传失败"}

用自定义错误类型封装业务语义,而非拼接字符串

避免用 fmt.Errorf("failed to save user: %w", err) 层层包裹后仍靠 err.Error() 提取信息;应定义可识别的错误类型,便于上层 switch 判断并映射为用户消息。

type ErrUserNotFound struct{ UserID int }
func (e *ErrUserNotFound) Error() string { return "user not found" }
func (e *ErrUserNotFound) StatusCode() int { return 404 }
func (e *ErrUserNotFound) UserMessage() string { return "用户不存在,请检查 ID" }

// 使用
if errors.As(err, &ErrUserNotFound{}) {
    http.Error(w, "用户不存在,请检查 ID", 404)
}
  • 不要依赖 strings.Contains(err.Error(), "no such file") 做判断——脆弱且不可维护
  • 每个业务错误类型实现 UserMessage() 方法,由 handler 统一调用,不散落在各处
  • HTTP handler 中用 errors.Aserrors.Is 匹配,而不是 ==strings 操作

HTTP handler 中统一错误转译,避免中间件吞掉关键上下文

常见错误是写个通用 error middleware,把所有 panic 和 error 都转成 500 Internal Server Error,结果本该是 400 的参数校验失败也变成 500,掩盖了真实问题。

  • 在 handler 内部尽早判断已知业务错误(如 *ErrInvalidEmail),直接返回 400 + 友好提示
  • 中间件只兜底未被显式处理的 error 和 panic,记录日志并返回泛化提示(如“服务暂时不可用”)
  • 对客户端暴露的字段名保持稳定:始终用 message 字段,不用 errormsgdesc 等混用
  • 敏感字段(如数据库错误码、SQL 语句)绝不能出现在响应体中,哪怕开发环境也要禁用

日志里保留原始错误,响应体里只放脱敏后消息

同一个错误对象,log 时要完整(含 stack trace、cause、context),但返回给前端时只取 UserMessage() 结果。二者必须分离,不能共用一个字符串输出。

err := db.QueryRow("SELECT ...").Scan(&u.ID)
if err != nil {
    log.Printf("user.load failed, user_id=%d, err=%+v", userID, err) // %+v 保留栈和 wrapped error
    http.Error(w, uErr.UserMessage(), uErr.StatusCode())
}
  • github.com/pkg/errors 或 Go 1.13+ fmt.Errorf("%w") 保留错误链,方便日志追踪
  • log.Printf 中的 %+v 是关键,它会展开 wrapped error 和 goroutine stack,而 %v 不会
  • 永远不要在 UserMessage() 实现里调用 log 或触发网络请求——它只负责返回纯文本
错误对外展示最易被忽略的一点:没有区分「开发者需要看到什么」和「用户需要看到什么」。同一错误在日志、监控告警、API 响应、前端 toast 中,内容、粒度、语言都该不同。硬编码一个字符串打天下,迟早踩坑。