不能直接用 errors.New 或 fmt.Errorf,因为它们返回的 *errors.errorString 无额外字段、无法携带上下文、不支持类型断言;应定义结构体实现 error 接口,并添加导出字段和辅助方法。
errors.New 或 fmt.Errorf?因为它们返回的是 *errors.errorString,没有额外字段、无法携带上下文(如错误码、请求ID、时间戳)、也不支持类型断言判断错误种类。比如你希望区分是数据库超时还是连接拒绝,仅靠错误消息字符串匹配脆弱且低效。
用结构体实现 error 接口(即实现 Error() string 方法),并按需添加字段。注意:结构体字段应导出(首字母大写)才能被外部访问,但 Error() 方法返回的字符串不应暴露敏感信息。
type DatabaseError struct {
Code int
Message string
ReqID string
}
func (e *DatabaseError) Error() string {
return e.Message
}
func (e *DatabaseError) ErrorCode() int {
return e.Code
}
Error() string 才算满足 error 接口ErrorCode())用于类型安全地提取信息返回时用 &DatabaseError{...} 构造指针;调用方用 errors.As 或类型断言识别具体类型,比字符串匹配可靠得多。
func QueryUser(id int) (string, error) {
if id <= 0 {
return "", &DatabaseError{
Code: 4001,
Message: "invalid user id",
ReqID: "req-abc123",
}
}
return "alice", nil
}
// 调用方
name, err := QueryUser(-1)
if err != nil {
var dbErr *DatabaseError
if errors.As(err, &dbErr) {
log.Printf("DB error %d for req %s", dbErr.Code, dbErr.ReqID)
}
}
errors.As 是 Go 1.13+ 推荐方式,能正确处理嵌套错误(如 fmt.Errorf("wrap: %w", err))err.(*DatabaseError) 仅适用于最外层错误,不推荐用于生产环境Error() 方法里拼接所有字段(如包含 ReqID),否则日志里会泄露敏感上下文Unwrap() 支持错误链?要。如果自定义 error 可能包装其他 error(比如重试失败后追加原因),就必须实现 Unwrap() error,否则 errors.Is/As 无法穿透到原始错误。
type WrappedError struct {
Msg string
Err error // 原始错误
Code int
}
func (e *WrappedError) Error() string { return e.Msg }
func (e *WrappedError) Unwr
ap() error { return e.Err }
func (e *WrappedError) ErrorCode() int { return e.Code }
error 类型且实现了 Unwrap(),errors 包就能递归展开Unwrap() 返回非 nil 时,errors.As 会按顺序尝试匹配,所以别乱返回Unwrap() 的自定义 error 在被 %w 包装后,会被当成普通字符串,丢失底层错误类型Unwrap() 是否到位、是否过度暴露内部细节,这三点比语法正确性更容易出问题。