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 错误构造与断言关系图

下图展示了标准库提供的三种错误构造方式及其对应的断言路径:

Go 标准库 error 构造与断言路径 errors.New(text) 静态字符串错误 fmt.Errorf(format, args...) 动态格式化错误 自定义 error 类型 携带领域上下文 errorString 结构体 s string 包装错误链 %w 动词 自定义结构体 Code + Message + Unwrap == 直接比较 精确匹配 errors.Is() 遍历错误链 errors.As() / 类型断言 提取领域上下文 布尔结果 true / false 布尔结果 true / false 领域错误实例 提取 Code 等字段 自定义 error 类型是领域错误断言的核心:通过 Code 字段精确匹配,通过 Unwrap 保持错误链兼容 errors.New 和 fmt.Errorf 适用于简单场景,自定义类型适用于领域边界

3.7 领域边界中的选择策略

在领域驱动设计中,错误类型的选择直接影响领域边界的清晰度:

  • 基础设施层:优先使用 fmt.Errorf 包装底层错误,保留错误链以便调试。
  • 领域层:必须使用自定义 error 类型,携带领域错误码和业务语义,避免暴露实现细节。
  • 应用层:组合领域错误与基础设施错误,通过 errors.Is 和 errors.As 进行断言路由。
推荐模式:在领域层定义 type DomainError struct 作为所有领域错误的基类型,子类型通过 Code 常量区分。这样既保持了类型统一,又支持细粒度断言。