17370845950

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

为什么直接用 errors.Newfmt.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()
  • 避免层层包裹却不检查——日志里看到一长串“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。