go自定义error的核心目的是实现可编程错误处理:通过结构体字段支持errors.is精确匹配、errors.as提取上下文、unwrap包装链和is自定义逻辑,解决字符串错误无法结构化解析、易失效、难监控等问题。

Go 里自定义 error 不是为了“看起来高级”,而是为了让你在 if err != nil 之后,能真正知道“错在哪、谁干的、要不要重试、能不能忽略”——字符串错误做不到这点。
为什么不能只用 errors.New 或 fmt.Errorf
它们返回的是无结构的 error,所有信息都挤在 Error() 返回的字符串里:
- 无法用
errors.Is(err, mySentinel)精确比对(除非你造全局哨兵变量,但那不解决动态上下文) - 无法用
errors.As(err, &e)提取字段(比如e.Field、e.Code) - 一旦错误文案微调(如把 “not found” 改成 “not exist”),
strings.Contains(err.Error(), "not found")就失效 - 日志或监控系统没法结构化解析错误码、重试标记等元数据
怎么定义一个带字段的自定义 error 类型
核心就三步:定义结构体 → 实现 Error() → 按需加 Unwrap() 和 Is()
- 结构体字段建议小写(如
code、retryable),避免暴露内部细节 -
Error()方法只返回用户友好的摘要,别拼接敏感数据(如 token、密码) - 如果要包装底层错误(比如 HTTP 请求失败后加业务语义),必须加
Err error字段 +Unwrap() error方法 - 如果希望支持
errors.Is(err, &TargetError{Code: 400})这种按字段匹配,就得实现Is()方法
示例:
type ValidationError struct {
Field string
Reason string
Code int
Retryable bool
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Reason)
}
func (e *ValidationError) Is(target error) bool {
t, ok := target.(*ValidationError)
if !ok {
return false
}
return e.Code == t.Code
}
什么时候该用哨兵错误,什么时候该用结构体
没有绝对好坏,只有场景适配:
- 用哨兵(如
var ErrNotFound = errors.New("not found")):表示固定、不可变的状态终点,比如io.EOF、sql.ErrNoRows。适合底层库导出的公开错误信号 - 用结构体(如
*ValidationError):需要携带动态上下文,比如参数校验失败时带Field和Value;或者需要区分错误等级、重试策略、监控标签等 - 混用常见:上层业务返回结构体错误,底层调用链中遇到
os.IsNotExist(err)这类系统哨兵时,用errors.Is(err, os.ErrNotExist)快速分流,再包装成业务结构体向上抛
容易被忽略的关键点
很多团队卡在最后一步:定义了结构体,却没让 errors.Is 和 errors.As 正常工作。
-
errors.As要求目标变量是指针(&e),不是值类型;传值会导致提取失败 -
Unwrap()返回nil表示“没包装其他错误”,返回非nil才参与标准错误链遍历 - 若结构体里有
Err error字段,Error()方法里不要直接调用e.Err.Error(),否则可能触发无限递归(尤其当e.Err也是同类型) -
Is()方法里做类型断言前,务必先检查是否为同一类型指针,否则 panic
真正难的不是写结构体,而是让整个错误链里每一层都遵循同一套可编程识别规则——否则上游拿到的永远只是一串日志里看不出来源的字符串。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











