应使用自定义错误结构体而非 errors.new 或 fmt.errorf,因其支持上下文携带、类型断言、错误分支区分;需实现 error() 方法,用指针类型定义,配合 errors.as/errors.is 判断,避免字符串比较或混用哨兵与结构体错误。

为什么不能只用 errors.New 或 fmt.Errorf
当错误需要携带上下文(如请求ID、失败行号、重试次数)、支持类型断言、或需区分不同错误分支(比如数据库连接失败 vs 记录未找到)时,errors.New 返回的纯字符串错误就力不从心了。它无法被程序逻辑识别,也无法附加结构化字段。
常见错误现象:多个地方都返回 fmt.Errorf("user not found"),上层只能靠字符串匹配判断——脆弱、易错、无法加额外元数据。
- 使用场景:API 错误码映射、重试策略判定、日志结构化打点、gRPC 状态码转换
- 性能影响:自定义错误类型本身无额外开销;但若在错误中存储大量对象(如整个
*http.Request),会延长错误生命周期并阻碍 GC - 兼容性:所有自定义错误必须实现
Error() string方法,否则无法满足error接口
如何定义可判断、可扩展的错误类型
推荐用结构体 + 实现 error 接口,并嵌入 fmt.Stringer 或直接实现 Error()。关键是要让错误可类型断言,且字段可读可查。
示例:
立即学习“go语言免费学习笔记(深入)”;
type UserNotFoundError struct {
UserID int64
ReqID string
RetryNum int
}
func (e *UserNotFoundError) Error() string {
return fmt.Sprintf("user not found: id=%d, req_id=%s", e.UserID, e.ReqID)
}
// 可选:支持 Unwrap 以兼容 errors.Is / errors.As(Go 1.13+)
func (e *UserNotFoundError) Unwrap() error { return nil }
- 务必用指针类型定义(
*UserNotFoundError)——值类型无法被errors.As正确识别 - 如果错误需嵌套其他错误(如“DB 查询失败导致用户未找到”),在结构体中加
err error字段,并在Unwrap()中返回它 - 避免在错误结构体中放不可序列化字段(如
sync.Mutex、函数、channel),否则 JSON 序列化或日志采集会 panic
如何在业务中正确使用和判断自定义错误
核心是用 errors.As 和 errors.Is 替代字符串比较,既安全又利于后期重构。
使用模板与程序分离的方式构建,依靠专门设计的数据库操作类实现数据库存取,具有专有错误处理模块,通过 Email 实时报告数据库错误,除具有满足购物需要的全部功能外,成新商城购物系统还对购物系统体系做了丰富的扩展,全新设计的搜索功能,自定义成新商城购物系统代码功能代码已经全面优化,杜绝SQL注入漏洞前台测试用户名:admin密码:admin888后台管理员名:admin密码:admin888
错误创建:
return &UserNotFoundError{UserID: 123, ReqID: r.Header.Get("X-Request-ID")}
错误判断:
if err != nil {
var notFoundErr *UserNotFoundError
if errors.As(err, ¬FoundErr) {
log.Warn("user not found", "user_id", notFoundErr.UserID, "req_id", notFoundErr.ReqID)
return http.StatusNotFound, nil
}
}
-
errors.As用于提取具体错误实例(含字段),errors.Is用于判断是否为某类错误(常配合Is()方法或哨兵变量) - 不要用
==比较两个自定义错误指针——它们永远不等,除非指向同一地址 - 若需全局唯一哨兵错误(如
ErrInvalidInput),定义为变量而非结构体实例,避免每次分配
什么时候该用哨兵错误,什么时候该用结构体错误
哨兵错误(如 var ErrPermissionDenied = errors.New("permission denied"))适用于无上下文、无需携带字段、仅作流程分支的简单场景;结构体错误适用于需携带上下文、支持分类处理、或未来可能扩展字段的场景。
容易踩的坑:
- 混用两者:比如对结构体错误用
errors.Is(err, ErrNotFound)—— 这永远返回 false,因为ErrNotFound是值,而实际错误是*UserNotFoundError实例 - 过度设计:一个只在单个函数内返回、且绝不会被上层捕获的错误,没必要定义新类型,
fmt.Errorf足够 - 忽略错误包装:调用下游函数后直接返回其错误(如
return db.QueryRow(...)),丢失当前层上下文;应使用fmt.Errorf("query user: %w", err)包装并保留原始错误链
真正难的是保持错误语义清晰——不是每个错误都需要自定义,但一旦定义,就要确保它能被稳定识别、安全解包、且字段含义不随版本模糊。









