错误信息不应直接返回原始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.As或errors.Is匹配,而不是==或strings操作
HTTP handler 中统一错误转译,避免中间件吞掉关键上下文
常见错误是写个通用 error middleware,把所有 panic 和 error 都转成 500 Internal Server Error,结果本该是 400 的参数校验失败也变成 500,掩盖了真实问题。
- 在 handler 内部尽早判断已知业务错误(如
*ErrInvalidEmail),直接返回 400 + 友好提示 - 中间件只兜底未被显式处理的
error和 panic,记录日志并返回泛化提示(如“服务暂时不可用”) - 对客户端暴露的字段名保持稳定:始终用
message字段,不用error、msg、desc等混用 - 敏感字段(如数据库错误码、SQL 语句)绝不能出现在响应体中,哪怕开发环境也要禁用
日志里保留原始错误,响应体里只放脱敏后消息
同一个错误对象,log 时要完整(含 stack trace、cause、context),但返回给前端时只取 UserMessage() 结果。二者必须分离,不能共用一个字符串输出。
立即学习“go语言免费学习笔记(深入)”;
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或触发网络请求——它只负责返回纯文本










