不能直接用 errors.New 或 fmt.Errorf,因为它们返回的 *errors.errorString 无额外字段、无法携带上下文、不支持类型断言;应定义结构体实现 error 接口,并添加导出字段和辅助方法。

为什么不能直接用 errors.New 或 fmt.Errorf?
因为它们返回的是 *errors.errorString,没有额外字段、无法携带上下文(如错误码、请求ID、时间戳)、也不支持类型断言判断错误种类。比如你希望区分是数据库超时还是连接拒绝,仅靠错误消息字符串匹配脆弱且低效。
如何定义带字段的自定义 error 类型?
用结构体实现 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())用于类型安全地提取信息 - 建议用指针接收者,避免值拷贝;若结构体含大字段或需修改状态,这点更关键
如何在函数中返回自定义 error 并做类型判断?
返回时用 &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 无法穿透到原始错误。
立即学习“go语言免费学习笔记(深入)”;
type WrappedError struct {
Msg string
Err error // 原始错误
Code int
}
func (e *WrappedError) Error() string { return e.Msg }
func (e *WrappedError) Unwrap() error { return e.Err }
func (e *WrappedError) ErrorCode() int { return e.Code }
- 只要结构体字段含
error类型且实现了Unwrap(),errors包就能递归展开 - 多个
Unwrap()返回非 nil 时,errors.As会按顺序尝试匹配,所以别乱返回 - 没实现
Unwrap()的自定义 error 在被%w包装后,会被当成普通字符串,丢失底层错误类型
Unwrap() 是否到位、是否过度暴露内部细节,这三点比语法正确性更容易出问题。









