Go 的错误处理哲学:error 是值,不是异常


一、Go 的错误处理哲学

1.1 与异常机制的区别

特性Go(error)Java/Python(异常)
表现形式普通返回值try-catch 机制
调用开销零(就是多返回一个值)较高(栈展开)
强制处理编译器检查(必须处理)可以不 catch
可预测性高——每个可能出错的地方都明确低——调用链可能随时抛异常

1.2 核心原则

// Go 的错误处理模式:每个可能出错的调用都检查返回值
result, err := doSomething()
if err != nil {
    // 处理错误:记录日志、返回错误、终止程序等
    return fmt.Errorf("doSomething failed: %w", err)
}
// 继续正常逻辑

Go 的哲学:显式胜于隐式。 没有隐藏的控制流,错误路径一目了然。


二、error 接口

2.1 error 的定义

// 标准库中 error 的定义(非常简洁)
type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都是一个 error:

// 自定义错误类型
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on field '%s': %s", e.Field, e.Message)
}

// 使用
err := &ValidationError{Field: "email", Message: "invalid format"}
fmt.Println(err)  // validation failed on field 'email': invalid format

2.2 创建错误的几种方式

import (
    "errors"
    "fmt"
)

// 1. errors.New:简单的静态错误消息
var ErrNotFound = errors.New("not found")
// 使用时
if err == ErrNotFound {
    // 处理特定错误
}

// 2. fmt.Errorf:格式化错误消息(最常用)
err := fmt.Errorf("user %d not found", userID)

// 3. %w 动词:包装(wrap)另一个错误
func getUser(id int) (*User, error) {
    err := db.Query("...")
    if err != nil {
        return nil, fmt.Errorf("getUser(%d): %w", id, err)
        // %w 保留了原始错误,可以用 errors.Is/As 检查
    }
    return &user, nil
}

// 4. 自定义错误类型(携带结构化信息)
// 见上方 ValidationError 示例

三、错误处理模式

3.1 基本模式

// 模式1:检查并返回
func processFile(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        return fmt.Errorf("reading %s: %w", path, err)
    }
    // 处理 data...
    return nil  // 成功时返回 nil
}

// 模式2:检查并记录日志 + 继续
data, err := fetchConfig()
if err != nil {
    log.Printf("config fetch failed, using defaults: %v", err)
    data = defaultConfig()
}

// 模式3:检查并终止
file, err := os.Open("critical.txt")
if err != nil {
    log.Fatal("无法打开关键文件:", err)
}
defer file.Close()

3.2 错误链(Error Wrapping)

Go 1.13+ 支持错误包装,可以构建错误链:

// 定义 Sentinel 错误
var (
    ErrNotFound  = errors.New("record not found")
    ErrPermission = errors.New("permission denied")
    ErrTimeout   = errors.New("request timeout")
)

// 包装错误
func findUser(id int) (*User, error) {
    result, err := db.Query("SELECT ...", id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, fmt.Errorf("user %d: %w", id, ErrNotFound)
        }
        return nil, fmt.Errorf("findUser(%d): %w", id, err)
    }
    return result, nil
}

// 调用方检查错误链
user, err := findUser(42)
if errors.Is(err, ErrNotFound) {
    // 处理"未找到"的特定情况
    fmt.Println("用户不存在")
} else if err != nil {
    // 其他错误
    fmt.Println("查询出错:", err)
}

3.3 errors.Is 与 errors.As

// 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)
    fmt.Println("底层错误:", pathErr.Err)
}

// errors.Unwrap:获取被包装的原始错误
wrappedErr := fmt.Errorf("context: %w", originalErr)
unwrapped := errors.Unwrap(err)  // originalErr

3.4 错误处理的"黄金范式"

// Go 中最典型、最频繁出现的代码模式:
result, err := someOperation()
if err != nil {
    return fmt.Errorf("someOperation in context: %w", err)
}

编写规范:

  1. 错误消息小写开头,不要加标点:fmt.Errorf("read config: %w", err)
  2. 包含上下文:哪个函数出错、什么参数、什么条件
  3. 用 %w 包装:保留原始错误,方便调用方判断
  4. 不要只用 %v:%v 丢失了错误链信息
  5. Sentinel 错误用 Err 前缀命名:ErrNotFound、ErrTimeout

四、panic 与 recover

4.1 panic — 不可恢复的致命错误

// panic 用于真正的"不应该发生"的情况
func divide(a, b int) int {
    if b == 0 {
        panic("division by zero")  // 不应该传入 0
    }
    return a / b
}

// 常见的 panic 场景
// - 数组/切片越界
// - 空指针解引用
// - map 写入 nil
// - 类型断言失败(不带 ok 检查时)

4.2 recover — 捕获 panic

// recover 只在 defer 中有效
func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()
    return a / b, nil
}

// 使用
result, err := safeDivide(10, 0)
if err != nil {
    fmt.Println(err)  // recovered from panic: division by zero
}

4.3 HTTP 服务器中的 recover

// 在 HTTP handler 中 recover,防止单个请求导致服务器崩溃
func RecoverMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic recovered: %v\n%s", err, debug.Stack())
                http.Error(w, "Internal Server Error", 500)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

4.4 panic 使用原则

场景使用 panic使用 error
配置错误(程序启动时)✅ panic("invalid config")
不可恢复的内部错误✅
运行时可恢复的错误✅
用户输入/外部数据✅
库函数对外接口✅ 永远用 error

原则:库代码绝不 panic,panic 留给应用层的"最后防线"。


五、错误处理最佳实践

5.1 不要忽略错误

// ❌ 不要这样做
data, _ := os.ReadFile("config.txt")  // 忽略错误

// ✅ 即使不处理,至少记录
data, err := os.ReadFile("config.txt")
if err != nil {
    log.Printf("warning: cannot read config: %v", err)
    // 使用默认值或继续
}

5.2 错误返回 nil 表示成功

// Go 的惯例:成功时 error 为 nil
func Find(id int) (*Item, error) {
    // 找到:返回 (item, nil)
    // 未找到:返回 (nil, ErrNotFound)
    // 出错:返回 (nil, fmt.Errorf("...: %w", err))
}

5.3 早期返回(Early Return)

// 使用 if err != nil → return 的"卫语句"风格
// 深度嵌套的 if-else 是反模式
func process(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        return fmt.Errorf("read: %w", err)
    }

    parsed, err := parse(data)
    if err != nil {
        return fmt.Errorf("parse: %w", err)
    }

    result, err := validate(parsed)
    if err != nil {
        return fmt.Errorf("validate: %w", err)
    }

    return save(result)
}

六、本节要点速查

概念要点
error 接口只需实现 Error() string 方法
errors.New创建简单错误
fmt.Errorf + %w创建带上下文的包装错误(最常用)
errors.Is检查错误链中是否包含特定错误值
errors.As从错误链中提取特定错误类型
Sentinel 错误包级 var ErrXxx = errors.New(...)
panic不可恢复的致命错误,库代码不用
recover在 defer 中捕获 panic
错误命名小写开头、含上下文、用 %w 包装
不要忽略 _每个 error 都要处理,或至少记录日志

上一篇:04-流程控制 下一篇:06-函数与方法