因为error是接口,字符串错误无法满足类型识别、携带上下文、结构化分类等业务需求;需用首字母大写的结构体实现Error()和Is()方法,并全程使用指针和%w包裹。

为什么 error 接口不能直接赋值字符串就当业务错误用
因为 Go 的 error 是个接口,定义为 type error interface { Error() string }。只写 errors.New("xxx") 或 fmt.Errorf("xxx") 得到的是基础错误,没有字段、无法区分类型、没法带状态码或请求 ID——线上一出错,你连是参数错还是 DB 超时都得靠字符串匹配。
- 业务错误必须能被
errors.Is()或errors.As()识别,否则下游无法做类型判断 - 字符串错误无法携带额外上下文(如订单号、用户 ID),日志和监控就只剩“用户名不能为空”这种裸信息
- HTTP 返回码、重试策略、告警分级都依赖错误的结构化分类,不是靠
strings.Contains(err.Error(), "timeout")
怎么用结构体实现可识别、可扩展的业务错误类型
核心是让自定义类型满足 error 接口,并支持类型断言。别用空接口或 map 模拟,要真 struct。
- 定义一个带字段的结构体,比如
type BizError struct { Code int `json:"code"` Msg string `json:"msg"` TraceID string `json:"trace_id,omitempty"` } - 必须实现
Error() string方法,返回人类可读信息(return e.Msg就行,别拼接 Code) - 如果需要被
errors.As()捕获,结构体字段不能全为私有(首字母小写),否则反射拿不到 - 推荐加一个
Is(target error) bool方法,方便做语义相等判断(比如不同实例但同 Code 可视为同一类错误)
func (e *BizError) Error() string { return e.Msg }
func (e *BizError) Is(target error) bool {
t, ok := target.(*BizError)
if !ok { return false }
return e.Code == t.Code
}
errors.Is 和 errors.As 为啥经常失效
不是 API 有问题,是你没按规则造错误实例。最常见三个坑:
- 用
&BizError{...}创建临时错误没问题,但若函数返回BizError值类型(没取地址),errors.As会失败——接口里存的是值,断言要的是指针 - 嵌套错误时(比如
fmt.Errorf("wrap: %w", err)),errors.Is只检查当前层和所有%w包裹链,但如果你用fmt.Errorf("wrap: %v", err)(用了%v),包裹关系就断了 - 多个包各自定义了同名
BizError,即使字段一样,errors.As也会失败——Go 认为它们是不同类型,哪怕长得一样
HTTP handler 里怎么把业务错误转成响应而不暴露内部细节
别在 handler 里写一堆 if errors.As(err, &authErr) { ... },容易漏、难维护。统一收口处理更稳。
立即学习“go语言免费学习笔记(深入)”;
- 定义错误映射表:比如
map[int]struct{ status int; exposeMsg bool },把BizError.Code映射到 HTTP 状态码和是否透出 Msg 给前端 - 中间件里用
errors.As提取*BizError,再查表决定http.Error的 status 和 body - 对非
*BizError的 panic 或系统错误,一律返回 500 + 通用提示,绝不透出err.Error()(防止路径、DB 表名泄露) - TraceID 必须从原始错误里提取并写入 response header(如
X-Trace-ID),方便前后端联调对齐日志
%w 包裹、不在任意一层降级成字符串。一旦某个中间函数偷偷调了 fmt.Sprintf("%s", err),后面所有类型判断就全废了。










