当错误需携带上下文、支持类型断言或扩展方法时,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 判断原始错误。
%w 才会保留原始错误;用 %s 就断链了Unwrap()
自定义错误结构体若含指针字段(如 *string),且未初始化,在 JSON 编码时可能 panic 或输出 null,而调用方误以为字段存在。更隐蔽的问题是:返回 nil *MyError 仍满足 error 接口(因为接口值本身非 nil),但解引用会 panic。
string 代替 *string)Error() 方法开头加 if e == nil { return "(nil error)" } 防止 panicjson.Marshal 当响应体——它不是数据结构,是运行时诊断信息;真要透出细节,显式定义 AsMap() 方法最易被忽略的是:错误类型的包路径变更会导致 errors.As 失败——跨模块时务必注意导入路径一致性,别因重命名包让下游的类型断言永远为 false。