3、Go标准库error接口:error接口定义、errors.New、fmt.Errorf、自定义error类型
Go语言的错误处理体系以标准库的 error 接口为基石。理解该接口的定义、工厂函数以及自定义扩展方式,是构建领域错误断言的前提。本节将逐一拆解这些核心要素。
3.1 error接口定义
Go语言通过一个极简的内置接口定义了错误契约:
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
该接口只包含一个方法 Error() string,任何实现了该方法的类型都自动满足 error 接口。这种设计带来了两个关键特性:
- 隐式实现:无需显式声明
implements,类型安全由编译器静态检查保障。 - 可组合性:任何携带错误信息的结构体都可以作为
error传递,为领域错误类型提供了扩展入口。
关键认知:
error 是一个接口,而非具体类型。这意味着 nil 检查需要同时关注接口的动态类型和动态值,否则可能引发隐蔽的 nil 非 nil 陷阱。
3.2 errors.New:最简错误工厂
errors.New 是标准库提供的最基础错误构造方式,它接收一个字符串并返回一个包含该字符串的 error 值:
package errors
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
使用示例:
var ErrNotFound = errors.New("entity not found")
func FindUser(id int) (*User, error) {
// ... 查询逻辑
return nil, ErrNotFound
}
最佳实践:
errors.New 创建的错误值通常声明为包级变量(如 ErrNotFound),以便调用方通过 == 操作符进行精确断言。注意:每次调用 errors.New 都会分配新的内存地址,即使字符串内容相同。
3.3 fmt.Errorf:带格式的错误构造
当错误信息需要动态拼接参数时,fmt.Errorf 提供了更灵活的方案:
func OpenFile(path string) error {
if path == "" {
return fmt.Errorf("invalid file path: %q", path)
}
// ... 打开文件逻辑
return nil
}
Go 1.13 之后,fmt.Errorf 支持通过 %w 动词包装错误,形成错误链:
func ReadConfig(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read config failed: %w", err)
}
// ... 解析配置
return nil
}
| 动词 | 行为 | 是否支持 errors.Is/As | 典型场景 |
|---|---|---|---|
%s |
格式化错误字符串 | 否 | 纯日志输出 |
%v |
默认格式输出 | 否 | 通用格式化 |
%w |
包装错误,建立错误链 | 是 | 错误传播与断言 |
注意:
%w 只能用于一个错误参数,且只能出现一次。如果需要包装多个错误,需使用 errors.Join(Go 1.20+)或第三方库。
3.4 自定义error类型
当需要携带额外上下文(如错误码、领域信息、堆栈追踪)时,自定义 error 类型是必然选择。只需实现 Error() string 方法即可:
// DomainError 携带领域错误码和上下文信息
type DomainError struct {
Code int
Message string
Err error // 可选:包装原始错误
}
func (e *DomainError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
// Unwrap 支持 errors.Is/As 遍历错误链
func (e *DomainError) Unwrap() error {
return e.Err
}
使用场景:
var (
ErrUserNotFound = &DomainError{Code: 1001, Message: "user not found"}
ErrInvalidInput = &DomainError{Code: 1002, Message: "invalid input"}
)
func GetUser(id int) (*User, error) {
if id <= 0 {
return nil, ErrInvalidInput
}
// ... 查询逻辑
return nil, ErrUserNotFound
}
领域边界意义:自定义
error 类型是领域错误断言的核心载体。通过 Code 字段可以精确匹配错误类别,Message 提供人类可读描述,Unwrap 方法保持与标准错误链机制的兼容。
3.5 三种错误构造方式对比
| 方式 | 适用场景 | 是否支持错误链 | 是否可携带额外数据 | 断言方式 |
|---|---|---|---|---|
errors.New |
静态错误消息,无需参数 | 否 | 否 | == 比较 |
fmt.Errorf |
动态格式化错误消息 | 是(%w) |
否 | errors.Is |
| 自定义类型 | 需要错误码、领域上下文、堆栈等 | 是(实现 Unwrap) |
是 | errors.As 或类型断言 |
3.6 错误构造与断言关系图
下图展示了标准库提供的三种错误构造方式及其对应的断言路径:
3.7 领域边界中的选择策略
在领域驱动设计中,错误类型的选择直接影响领域边界的清晰度:
- 基础设施层:优先使用
fmt.Errorf包装底层错误,保留错误链以便调试。 - 领域层:必须使用自定义
error类型,携带领域错误码和业务语义,避免暴露实现细节。 - 应用层:组合领域错误与基础设施错误,通过
errors.Is和errors.As进行断言路由。
推荐模式:在领域层定义
type DomainError struct 作为所有领域错误的基类型,子类型通过 Code 常量区分。这样既保持了类型统一,又支持细粒度断言。