go自定义错误类型旨在区分错误种类并携带上下文字段,应使用导出字段的Struct实现Error接口,配合errors.Is/As进行健壮判断,避免敏感数据和大对象,确保调用链中类型穿透一致。

Go 里自定义错误类型不是为了“看起来更专业”,而是为了解决两个实际问题:能区分错误种类(比如 IsTimeoutErr 还是 IsAuthFailed),以及能携带上下文字段(比如失败的 UserID、重试次数 RetryCount)。
用 struct 实现 error 接口是最常用且可控的方式
Go 的 error 就是个接口:type error Interface { Error() String }。只要实现这个方法,就是合法错误。用 struct 而非 string 或 fmt.Errorf 包装,是为了可扩展性。
常见错误写法是直接返回 fmt.Errorf("user %d not found", id) —— 这类错误无法被程序逻辑识别,只能靠字符串匹配,脆弱且不可靠。
正确做法是定义一个结构体:
立即学习“go语言免费学习笔记(深入)”;
type UserNotFoundError struct { UserID int Source string // 可选:标记来自 DB 还是 Cache } func (e *UserNotFoundError) Error() string { return fmt.Sprintf("user %d not found in %s", e.UserID, e.Source) }
这样调用方就能用类型断言精准识别:
if err != nil { if _, ok := err.(*UserNotFoundError); ok { // 触发降级逻辑,不记录告警 return fallbackData() } // 其他错误走通用处理 }
- 务必导出结构体字段(首字母大写),否则外部包无法访问
UserID等上下文 - 不要在
Error()方法里做耗时操作(如日志、网络请求),它可能被多次调用 - 如果错误不需要额外字段,用
errors.New("xxx")更轻量,不必强上 struct
用 errors.Is 和 errors.As 替代类型断言
Go 1.13 引入了 errors.Is 和 errors.As,它们能穿透多层包装错误(比如被 fmt.Errorf("wrap: %w", err) 包过),比裸类型断言更健壮。
前提是你的自定义错误也支持 Unwrap() 方法:
func (e *UserNotFoundError) Unwrap() error { return nil // 表示它是最底层错误;若包装了其他 error,这里返回那个 error }
然后就可以安全地跨层级判断:
err := doSomething() // 可能返回 fmt.Errorf("DB query failed: %w", &UserNotFoundError{UserID: 123}) var notFoundErr *UserNotFoundError if errors.As(err, ¬FoundErr) { log.Printf("user %d missing", notFoundErr.UserID) }
- 只要任意一层错误是
*UserNotFoundError类型,errors.As就能捕获到 - 如果自定义错误内部包装了另一个 error(比如数据库驱动错误),就在
Unwrap()返回它,别返回nil -
errors.Is(err, someErr)适合判断是否等于某个预定义的哨兵错误(如io.EOF),不适合带字段的自定义类型
避免在错误中存敏感数据或大对象
错误值可能被长期持有、打印、序列化进日志甚至上报到监控系统。如果结构体里塞了 RawRequest []byte 或 UserPassword string,就构成泄露风险。
典型反例:
type BadRequestError struct { RawBody []byte // ❌ 日志一打全暴露 User *User // ❌ 可能含密码字段 }
- 只保留诊断必需的最小字段:ID、状态码、时间戳、简短原因码(如
"invalid_token") - 需要调试原始数据?用独立的 debug 日志,而不是塞进 error 实例
- 结构体大小要小——错误频繁创建,大结构体会增加 GC 压力
真正难的不是定义一个 MyCustomError,而是在整个调用链里保持错误类型的穿透性和语义一致性。比如 http handler 捕获到 *UserNotFoundError,不该转成 fmt.Errorf("internal error") 吞掉原类型;中间件加 wrap 时也要确保 Unwrap() 正确返回下层错误。这些细节漏掉一处,自定义错误就退化成普通字符串。