Go中error是接口,需实现Error()方法;用errors.New/fmt.Errorf创建基础错误,%w包装保留链;自定义错误应含元信息并指针接收;用errors.Is/As判断类型而非字符串匹配。

Go 语言中 error 接口本身不支持直接“创建错误对象”,它只是一个约定:只要实现了 Error() string 方法的类型,就是 error。真正创建错误实例的是 errors.New、fmt.Errorf 或自定义结构体——关键不在接口,而在如何让错误携带足够信息且易于诊断。
用 errors.New 和 fmt.Errorf 快速构造基础错误
这是最常见也最容易误用的方式。两者都返回 *errors.errorString(底层是带字符串字段的私有结构体),但语义不同:
-
errors.New("file not found")适合无上下文、静态错误消息,比如预定义的错误常量 -
fmt.Errorf("open %s: %w", filename, err)(带%w)才能正确包装错误,保留原始错误链;漏掉%w就断链,errors.Is/errors.As会失效 - 避免
fmt.Errorf("something failed: %v", err)—— 这会把原错误转成字符串,丢失类型和额外字段
自定义 error 类型时必须实现 Error() 方法
当你需要错误携带状态码、请求 ID、重试建议等元信息时,就得定义结构体。注意:Error() 方法签名必须严格匹配 func() string,且不能指针接收者混用(除非你明确控制零值行为):
type ValidationError struct {
Field string
Message string
Code int
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s (code: %d)", e.Field, e.Message, e.Code)
}
// ✅ 正确:指针接收者,可修改字段,且 nil *ValidationError 调用 Error() 不 panic
// ❌ 错误:值接收者 + 字段为零值时 Error() 返回空字符串,易被忽略
用 errors.Is 和 errors.As 判断错误类型而非字符串匹配
字符串比较(如 strings.Contains(err.Error(), "timeout"))脆弱且不可靠。Go 1.13+ 提供了基于错误链的判断方式:
立即学习“go语言免费学习笔记(深入)”;
-
errors.Is(err, io.EOF)检查是否等于某个已知错误(支持包装链) -
errors.As(err, &e)尝试向下转型到自定义错误类型(e是对应类型的变量) - 前提是错误链中至少有一个节点是目标类型;如果用了
fmt.Errorf("%v", err)替代%w,As就永远失败
错误包装不是越多越好:避免过度包装导致堆栈冗余
每层 fmt.Errorf("...: %w", err) 都增加一层包装,errors.Unwrap 可逐层解包,但调试时看 err.Error() 会连缀出一长串前缀。实际工程中应:
- 在边界处包装(如 HTTP handler、数据库调用入口),带上上下文(如
"failed to persist user in db") - 中间逻辑尽量透传或用
%w精准包装,不无意义地加 “failed to …” 前缀 - 日志记录时用
fmt.Printf("%+v", err)(需第三方库如github.com/pkg/errors或 Go 1.22+ 的内置格式化)才显示完整链;默认%v只显示最外层
真正难的不是实现 Error() 方法,而是决定什么时候该新建一个错误类型、什么时候该复用已有错误、以及在多深的调用栈里做包装——这些没有标准答案,取决于你的可观测性需求和团队对错误分类的共识。










