应定义可识别、可扩展的自定义错误类型,实现error接口并携带errorcode()等方法;必须用%w包装以维持错误链,支持errors.is/as;统一businesserror接口便于中间件提取状态码与元信息。

Go 里怎么定义业务错误而不是 errors.New 硬编码
直接用 errors.New("user not found") 写业务错误,后期根本没法判断类型、加上下文、做重试或监控。真正要的是可识别、可扩展、能带字段的错误。
推荐用自定义错误类型,实现 error 接口,并附带业务码和元信息:
type UserNotFoundError struct {
UserID int
Code string // "USER_NOT_FOUND"
}
func (e *UserNotFoundError) Error() string {
return "user not found"
}
func (e *UserNotFoundError) ErrorCode() string {
return e.Code
}
- 别用
fmt.Errorf("user %d not found", id)包裹后就丢——它丢失了结构,errors.Is和errors.As都抓不住 - 如果错误不需要携带字段,至少用
var ErrUserNotFound = errors.New("user not found")全局变量,方便errors.Is(err, ErrUserNotFound) - 所有业务错误建议统一继承一个嵌入接口(如
type BusinessError interface { error; ErrorCode() string }),后续中间件统一提取码
为什么 errors.Is 和 errors.As 在业务错误里常失效
不是函数有问题,而是你没按它们的设计逻辑构造错误链。它们只认“包装”关系(wrapping),不认字符串匹配或类型断言。
- 必须用
fmt.Errorf("wrap: %w", originalErr)才算“包装”,%w是关键;用%s或拼接字符串就断链了 -
errors.As(err, &target)要求target是指针,且原始错误或其任意包装层实现了对应类型——所以自定义错误一定要导出字段或方法,别全小写 - HTTP handler 里用
log.Printf("err: %+v", err)查看完整错误栈,确认%w是否真被传递下去
HTTP 错误响应时怎么把业务错误转成状态码和 JSON
不能靠 switch err.Error(),既脆弱又难测。应该让错误自己决定 HTTP 状态和响应体。
立即学习“go语言免费学习笔记(深入)”;
func (e *UserNotFoundError) StatusCode() int { return 404 }
func (e *UserNotFoundError) Response() map[string]any {
return map[string]any{"code": e.Code, "message": "user does not exist"}
}
- 在中间件里统一调用
err.(interface{ StatusCode() int }).StatusCode()(先errors.As判断是否实现了该接口) - 避免在每个 handler 里写
if errors.Is(err, ErrUserNotFound)——错误分类逻辑下沉到错误类型自身 - 注意:不要让错误类型依赖 HTTP 包(如
http.StatusNotFound),用整数字面量或自定义常量,保持 domain 层纯净
日志和监控里怎么区分业务错误和系统错误
日志里满屏 error="user not found" 没意义。关键是要让错误自带“意图”标签,而不是靠消息文本猜。
- 给每个业务错误类型加一个
Kind()方法,返回"business"/"validation"/"external"等语义标识 - 监控告警时过滤
err.Kind() == "business"的错误,不告警;只对"system"或"panic"类型触发告警 - 别在错误消息里塞敏感字段(如密码、token),字段值该打码就打码——
Error()方法只返回安全提示,具体数据走独立字段供日志采集器选择性打印
最麻烦的其实是错误的生命周期管理:谁负责包装、谁负责解包、谁负责透传到底层。一旦某层用了 fmt.Errorf("%v", err) 而不是 %w,整条链就断了,后面所有分类、重试、监控都失灵。这点比定义错误类型本身还容易被忽略。










