错误处理

Go 将错误视为普通值,通过返回值显式传递与处理,而非异常机制。这使控制流清晰可见,是 Go 代码最显著的风格特征。

error 接口

error 是一个仅含一个方法的内置接口:

type error interface {
    Error() string
}

任何实现了 Error() string 的类型都可作为错误使用。

创建错误

package main

import (
    "errors"
    "fmt"
)

func main() {
    // 1. errors.New:创建简单错误
    err1 := errors.New("用户不存在")

    // 2. fmt.Errorf:格式化错误信息
    userID := 42
    err2 := fmt.Errorf("查询用户 %d 失败", userID)

    fmt.Println(err1, err2)
}

自定义错误类型

当调用方需要依据错误类型分支处理时,定义结构体错误:

package main

import (
    "errors"
    "fmt"
)

type ValidationError struct {
    Field  string
    Reason string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("字段 %s 校验失败: %s", e.Field, e.Reason)
}

func validate(age int) error {
    if age < 0 {
        return &ValidationError{Field: "age", Reason: "不能为负数"}
    }
    return nil
}

func main() {
    err := validate(-1)
    if err != nil {
        var ve *ValidationError
        if errors.As(err, &ve) {
            fmt.Println("字段:", ve.Field) // 按类型取出详细信息
        }
    }
}

错误包装与判定

实际项目中错误会跨函数层传递。Go 1.13 引入 %w 动词与 errors 包,支持包装并保留错误链:

package main

import (
    "errors"
    "fmt"
    "os"
)

var ErrNotFound = errors.New("record not found") // 哨兵错误

func loadUser(id int64) error {
    _, err := os.Open("user.json")
    if err != nil {
        return fmt.Errorf("加载用户 %d: %w", id, err) // %w 包装底层错误
    }
    return nil
}

func main() {
    err := loadUser(1)

    // errors.Is:沿错误链匹配哨兵错误
    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("文件不存在")
    }

    // errors.As:沿错误链提取特定错误类型
    var pathErr *os.PathError
    if errors.As(err, &pathErr) {
        fmt.Println("路径错误:", pathErr.Path)
    }

    // 哨兵错误对比
    if errors.Is(err, ErrNotFound) {
        fmt.Println("记录未找到")
    }

    fmt.Println(err) // 打印完整错误链信息
}
Tip

三种判定方式的选择:

  • errors.Is:按匹配,适合预定义的哨兵错误(如 os.ErrNotExist)。
  • errors.As:按类型匹配,适合携带上下文的结构体错误。
  • 直接比较 err == ErrNotFound:仅适用于未包装的错误,包装后失效,不推荐。

约定俗成:包内可导出的哨兵错误以 Err 开头,如 ErrNotFound

panic 与 recover

panic 表示不可恢复的程序错误(如数组越界、空指针),将中断当前函数调用栈;recover 仅在 defer 中有效,用于捕获 panic 恢复执行:

package main

import "fmt"

func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("运行时错误已恢复: %v", r)
        }
    }()
    return a / b, nil // b 为 0 时 panic
}

func main() {
    v, err := safeDivide(10, 0)
    fmt.Println(v, err) // 0 运行时错误已恢复: runtime error: integer divide by zero
}
Warning

使用准则:error 用于预期内的失败,panic 用于程序缺陷。业务代码中不应主动使用 panic 表达错误;HTTP 框架通常在请求入口统一 recover,将 panic 转换为 500 响应,保证单个请求崩溃不拖垮整个服务。

小结

  • 错误是值:if err != nil 是 Go 的标准错误处理范式。
  • %w 包装错误保留上下文,errors.Is/As 精确判定。
  • panic 只用于不可恢复缺陷,recover 提供兜底能力。