当错误需携带上下文、支持类型断言或扩展方法时,errors.New/fmt.Errorf 不足;应定义实现error接口的导出结构体(如*NotFoundError),用errors.As安全识别,并注意nil指针、JSON序列化及包路径一致性。

为什么直接用 errors.New 或 fmt.Errorf 不够用
当错误需要携带上下文(比如请求 ID、失败的文件路径、重试次数)、支持类型断言判断错误种类,或需实现 Error() 以外的方法(如 Timeout()、Retryable())时,基础错误构造函数就力不从心了。Go 的错误本质是接口:type error interface { Error() string },只要满足这个契约就能当错误用——所以自定义类型只需实现它,但更进一步,它还能带字段、方法和行为。
如何定义可判断类型的自定义错误结构体
关键点不是“怎么写结构体”,而是“怎么让调用方能安全识别并处理它”。推荐方式是导出错误类型,并让其实现 error 接口:
type NotFoundError struct {
Path string
Code int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("not found: %s (code %d)", e.Path, e.Code)
}
func (e *NotFoundError) IsNotFound() bool {
return true
}
使用时可类型断言:
if err != nil {
var nf *NotFoundError
if errors.As(err, &nf) {
log.Printf("missing resource: %s", nf.Path)
return
}
}
- 必须用指针类型(
*NotFoundError)实现error,否则errors.As无法匹配 - 不要在
Error()中 panic 或访问未初始化字段,日志/HTTP 中间件可能随时调用它 - 若错误类型仅用于标识(无额外字段),可用私有结构体 + 导出变量,更轻量:
var ErrInvalidToken = &invalidTokenError{}
type invalidTokenError struct{}
func (*invalidTokenError) Error() string { return "invalid auth token" }
何时该用 fmt.Errorf 包裹而不是新建类型
包裹(wrap)适用于“错误链”场景:底层出错,上层加一层上下文,但不改变错误语义。此时用 fmt.Errorf("read config: %w", err),再配合 errors.Is/errors.As 判断原始错误。
立即学习“go语言免费学习笔记(深入)”;
- 用
%w才会保留原始错误;用%s就断链了 - 如果包裹后还需暴露新字段(如重试策略),就得组合:自定义类型内部嵌入原错误 + 实现
Unwrap() - 避免层层包裹却不检查——日志里看到一长串“failed to … because failed to … because …”却没法针对性处理
常见陷阱:JSON 序列化自定义错误和 nil 指针
自定义错误结构体若含指针字段(如 *string),且未初始化,在 JSON 编码时可能 panic 或输出 null,而调用方误以为字段存在。更隐蔽的问题是:返回 nil *MyError 仍满足 error 接口(因为接口值本身非 nil),但解引用会 panic。
- 初始化所有字段,或用零值安全的类型(如
string代替*string) - 在
Error()方法开头加if e == nil { return "(nil error)" }防止 panic - 别把自定义错误直接塞进
json.Marshal当响应体——它不是数据结构,是运行时诊断信息;真要透出细节,显式定义AsMap()方法
最易被忽略的是:错误类型的包路径变更会导致 errors.As 失败——跨模块时务必注意导入路径一致性,别因重命名包让下游的类型断言永远为 false。










