go自定义error必须实现error()方法,否则if err != nil不生效;还需实现unwrap()和is()以支持errors.as()和errors.is(),并注意json序列化字段导出与标签。

Go里自定义error类型必须实现Error()方法
Go的error接口就一个方法:Error() string。只要你的结构体实现了它,就是合法的error。别想绕过这个——不实现它,哪怕字段再丰富,if err != nil也永远进不去分支。
常见错误是只加了字段、忘了写Error(),或者返回空字符串,结果日志里打印出来是<nil></nil>或空白,排查时一头雾水。
- 必须返回有意义的字符串,通常拼接关键字段(比如
code、message、traceID) - 不要在
Error()里做耗时操作(如调用fmt.Sprintf拼接大量上下文),影响错误路径性能 - 如果需要携带原始错误,用
Unwrap()方法(Go 1.13+),否则上层调用errors.Is()或errors.As()会失败
type AppError struct {
Code int
Message string
TraceID string
Err error // 原始底层错误
}
func (e *AppError) Error() string {
return fmt.Sprintf("code=%d msg=%s trace=%s", e.Code, e.Message, e.TraceID)
}
func (e *AppError) Unwrap() error { return e.Err }
带堆栈信息的error要用fmt.Errorf加%w或errors.Join
单纯实现Error()没法自动捕获调用栈。想让panic或日志里看到出错位置,得靠fmt.Errorf的%w动词包装,或者用errors.Join组合多个错误。
典型坑:自己写了Error()方法,又手动调用fmt.Errorf("wrap: %v", err)但没加%w,导致errors.Is()失效,且堆栈断在包装处,不是原始出错点。
立即学习“go语言免费学习笔记(深入)”;
-
fmt.Errorf("failed to read: %w", ioErr)→ 保留原始堆栈和可判定性 -
fmt.Errorf("failed to read: %v", ioErr)→ 堆栈丢失,errors.Is(ioErr)返回false - 多个错误合并用
errors.Join(err1, err2),比拼接字符串更利于下游解析
区分业务错误和系统错误:用errors.Is判断而不是==
自定义error类型一旦涉及多实例(比如不同Code的AppError),就不能用==比较。因为每次&AppError{Code: 404}都是新地址,err == someErr永远为false。
正确方式是给错误类型定义导出的变量(哨兵错误),或用errors.Is()配合Is()方法。
- 定义哨兵:
var ErrNotFound = &AppError{Code: 404, Message: "not found"} - 实现
Is(target error) bool方法,支持按Code或语义匹配 - 调用方统一用
errors.Is(err, ErrNotFound)或errors.Is(err, context.Canceled)
func (e *AppError) Is(target error) bool {
if t, ok := target.(*AppError); ok {
return e.Code == t.Code
}
return false
}
JSON序列化自定义error时字段名要对齐json:标签
很多服务把error结构体直接json.Marshal返回给前端,这时字段名大小写、是否导出、json:标签都直接影响输出。常见问题:字段全小写没加json:,结果序列化出来是空对象{};或者用了json:"-" 却忘了去掉敏感字段(比如Err里的内部错误详情)。
- 所有要暴露的字段必须首字母大写(导出)
- 显式加
json:标签控制键名,避免意外驼峰或下划线 - 敏感字段(如原始
Err、调试用Stack)加上json:"-"或设为小写不导出 - 测试一下
json.Marshal(&AppError{Code: 500})输出是否符合API文档约定
最常被忽略的是Unwrap()和Is()这两个方法——它们不写,errors.As()就抽不出你的自定义类型,errors.Is()也判不准。不是“能跑就行”,而是“不用它们,下游根本没法可靠处理”。










