2. error 接口:内置 error 接口的定义、实现自定义错误类型、errors.New 与 fmt.Errorf 基础用法

Go 语言通过内置的 error 接口统一了错误表示。理解该接口的定义、实现方式以及标准库提供的构造工具,是后续学习错误码断言与业务错误处理的基础。本节将围绕接口定义、自定义错误类型、errors.New 与 fmt.Errorf 三个核心知识点展开。

2.1 内置 error 接口定义

Go 的 error 是一个预声明接口类型,定义在 builtin 包中。它只包含一个方法:

// 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 接口。这意味着 Go 的错误处理基于隐式接口实现,无需显式声明 implements 关键字。

特性 说明
方法签名 Error() string
返回值 返回描述错误信息的字符串
零值 nil 表示没有错误
实现方式 隐式接口实现,任何类型只要定义了该方法即满足接口
关键认知: error 是 Go 中唯一用于表示错误的接口类型。函数返回 error 时,调用方应始终检查其是否为 nil。这是 Go 错误处理的基本契约。

2.2 实现自定义错误类型

虽然标准库提供了基础构造工具,但在业务系统中,我们通常需要携带更多上下文信息。自定义错误类型可以附加错误码、时间戳、堆栈信息等字段。

示例:定义一个携带错误码的业务错误类型

// BizError 业务错误类型,包含错误码和描述信息
type BizError struct {
    Code    int    // 业务错误码
    Message string // 人类可读的错误描述
}

// Error 实现 error 接口
func (e *BizError) Error() string {
    return fmt.Sprintf("code=%d, message=%s", e.Code, e.Message)
}

// 使用示例
func GetUser(id int) (*User, error) {
    if id <= 0 {
        return nil, &BizError{
            Code:    1001,
            Message: "invalid user id",
        }
    }
    // 正常业务逻辑...
    return &User{ID: id, Name: "Alice"}, nil
}

自定义错误类型的常见模式:

  • 结构体 + 字段:适用于需要携带多个上下文属性的场景,如错误码、请求 ID、用户 ID。
  • 哨兵错误 + 包装:定义全局哨兵错误,再通过包装附加额外信息。
  • 枚举错误码 + 错误接口:将错误码定义为枚举类型,统一管理。
实践建议: 在业务项目中,推荐将自定义错误类型定义在独立的 errors 或 errtypes 包中,避免循环依赖。错误码应使用常量或枚举,便于维护和断言。

2.3 errors.New 与 fmt.Errorf 基础用法

标准库提供了两个最常用的错误构造函数:errors.New 和 fmt.Errorf。它们都返回实现了 error 接口的值。

2.3.1 errors.New

errors.New 接收一个字符串参数,返回一个包含该字符串的错误值。它是最简单的错误构造方式。

import "errors"

var ErrNotFound = errors.New("record not found")
var ErrPermissionDenied = errors.New("permission denied")

func FindRecord(id int) (Record, error) {
    if id <= 0 {
        return Record{}, ErrNotFound
    }
    // 查找逻辑...
    return Record{}, nil
}

特点:

  • 返回的错误值不可变,每次调用返回相同的指针(如果字符串相同)。
  • 适合定义全局哨兵错误,用于调用方通过 == 或 errors.Is 进行断言。
  • 不支持格式化字符串,无法动态拼接上下文信息。

2.3.2 fmt.Errorf

fmt.Errorf 支持格式化字符串,可以动态构造错误信息。它返回一个包含格式化后字符串的错误值。

import "fmt"

func OpenFile(path string) (*os.File, error) {
    if path == "" {
        return nil, fmt.Errorf("invalid file path: %s", path)
    }
    // 打开文件逻辑...
    return os.Open(path)
}

特点:

  • 支持 %s、%d、%v 等格式化动词。
  • 每次调用返回一个新的错误值(即使格式化结果相同)。
  • Go 1.13 之后,fmt.Errorf 支持 %w 动词进行错误包装(将在后续章节详细讲解)。
对比维度 errors.New fmt.Errorf
参数 固定字符串 格式化字符串 + 可变参数
动态内容 不支持 支持
返回值复用 相同字符串返回相同指针 每次调用返回新值
适用场景 哨兵错误、静态错误 动态错误信息、包装错误
错误包装(%w) 不支持 支持(Go 1.13+)
常见误区: 不要将 errors.New 用于需要动态拼接错误信息的场景,否则每次拼接都会产生新的字符串,且无法利用哨兵错误的断言优势。此时应优先使用 fmt.Errorf 或自定义错误类型。

2.4 错误构造与断言流程

下面的 SVG 图展示了从错误定义、构造到调用方断言的整体流程,帮助理解 error 接口在业务代码中的典型使用链路。

错误构造与断言流程 定义错误类型 哨兵错误 / 自定义结构体 构造错误值 errors.New / fmt.Errorf 函数返回 error return nil / return err 调用方断言 if err != nil { 处理错误 } errors.Is(err, ErrNotFound) / 类型断言 (e *BizError) switch e := err.(type) { case *BizError: ... } 💡 核心要点 • 定义阶段:选择哨兵错误或自定义结构体,决定错误携带的信息量。 • 断言阶段:根据业务需求选择 errors.Is、类型断言或 errors.As 进行精确匹配。

2.5 综合示例:哨兵错误 + 自定义错误类型

以下示例展示了如何在实际业务中组合使用 errors.New 和自定义错误类型:

package service

import (
    "errors"
    "fmt"
)

// 哨兵错误定义
var (
    ErrUserNotFound = errors.New("user not found")
    ErrInvalidInput = errors.New("invalid input")
)

// BizError 业务错误,携带错误码
type BizError struct {
    Code    int
    Message string
    Err     error // 原始错误,用于错误链
}

func (e *BizError) Error() string {
    return fmt.Sprintf("code=%d, message=%s, cause=%v", e.Code, e.Message, e.Err)
}

func (e *BizError) Unwrap() error {
    return e.Err
}

// GetUserProfile 获取用户资料
func GetUserProfile(userID int) (*Profile, error) {
    if userID <= 0 {
        return nil, fmt.Errorf("%w: userID=%d", ErrInvalidInput, userID)
    }

    profile, err := db.FindProfile(userID)
    if err != nil {
        return nil, &BizError{
            Code:    2001,
            Message: "database query failed",
            Err:     err,
        }
    }

    return profile, nil
}

// 调用方断言示例
func HandleRequest(userID int) {
    profile, err := GetUserProfile(userID)
    if err != nil {
        if errors.Is(err, ErrInvalidInput) {
            // 处理无效输入
            fmt.Println("invalid input:", err)
        } else if errors.Is(err, ErrUserNotFound) {
            // 处理用户不存在
            fmt.Println("user not found:", err)
        } else {
            // 处理其他业务错误
            var bizErr *BizError
            if errors.As(err, &bizErr) {
                fmt.Printf("business error: code=%d, message=%s\n", bizErr.Code, bizErr.Message)
            }
        }
        return
    }
    // 正常处理 profile
    fmt.Println("profile:", profile)
}
学习要点: 本示例展示了三种错误构造方式的协同使用:errors.New 定义哨兵错误、fmt.Errorf 包装哨兵错误附加动态信息、自定义 BizError 结构体携带错误码。调用方通过 errors.Is 和 errors.As 进行分层断言,这是 Go 业务错误处理的标准模式。