znlgis 博客

GIS开发与技术分享 — GDAL · GeoServer · PostGIS · QGIS · OpenLayers · Cesium · FreeCAD · NPOI

第07章 - 错误处理机制

错误处理是 Go 语言设计哲学的重要体现。与许多语言使用异常(exception)不同,Go 将错误作为普通的返回值显式处理,使错误流清晰可见。本章系统讲解 Go 的错误处理机制,包括 error 接口、错误包装、panic 与 recover。

7.1 error 接口

Go 的错误以 error 接口表示,它是内置接口,定义极其简单:

type error interface {
    Error() string
}

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

7.2 错误处理的基本模式

Go 的惯用模式是:函数将 error 作为最后一个返回值,调用者立即检查:

func readConfig(path string) (string, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return "", err // 向上传播错误
    }
    return string(data), nil
}

// 调用方
content, err := readConfig("config.yaml")
if err != nil {
    log.Fatal(err) // 处理错误
}
fmt.Println(content)

这种 if err != nil 的检查在 Go 代码中无处不在。虽然略显冗长,但它强制开发者正视每一个可能的错误,避免错误被悄悄忽略。

7.3 创建错误

7.3.1 errors.New

创建简单的字符串错误:

import "errors"

var ErrNotFound = errors.New("记录未找到")

func find(id int) error {
    if id < 0 {
        return errors.New("id 不能为负数")
    }
    return nil
}

7.3.2 fmt.Errorf

需要格式化错误信息时使用:

func validate(age int) error {
    if age < 0 || age > 150 {
        return fmt.Errorf("年龄 %d 不在合法范围内", age)
    }
    return nil
}

7.3.3 哨兵错误(Sentinel Error)

预先定义的、可供比较的错误变量称为哨兵错误,常用于表示特定的错误状态:

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

if err == ErrNotFound {
    // 特殊处理
}

标准库中的 io.EOFsql.ErrNoRows 都是典型的哨兵错误。

7.4 自定义错误类型

当需要携带更多上下文信息时,可定义实现了 error 接口的结构体:

type ValidationError struct {
    Field   string
    Value   interface{}
    Message string
}

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

func validateName(name string) error {
    if name == "" {
        return &ValidationError{
            Field:   "name",
            Value:   name,
            Message: "不能为空",
        }
    }
    return nil
}

调用者可以通过类型断言提取详细信息:

err := validateName("")
var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Println("出错字段:", ve.Field)
}

7.5 错误包装与解包(Go 1.13+)

Go 1.13 引入了错误包装机制,可以在传播错误时附加上下文,同时保留原始错误,形成错误链。

7.5.1 包装错误:%w

fmt.Errorf 中使用 %w 动词包装一个错误:

func loadUser(id int) error {
    err := queryDB(id)
    if err != nil {
        return fmt.Errorf("加载用户 %d 失败: %w", id, err)
    }
    return nil
}

7.5.2 errors.Is:判断错误链中是否包含特定错误

err := loadUser(1)
if errors.Is(err, sql.ErrNoRows) {
    // 即使 err 被多层包装,也能正确判断
    fmt.Println("用户不存在")
}

errors.Is 会沿着错误链逐层解包比较,替代了直接用 == 比较的局限。

7.5.3 errors.As:提取错误链中特定类型的错误

var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Println("校验错误字段:", ve.Field)
}

7.5.4 errors.Unwrap:手动解包

wrapped := fmt.Errorf("外层: %w", ErrNotFound)
inner := errors.Unwrap(wrapped) // 返回 ErrNotFound

最佳实践:在跨越函数/模块边界传播错误时,使用 %w 添加上下文;在需要根据错误类型决策时,使用 errors.Iserrors.As,而非直接比较或类型断言。

7.6 panic 与 recover

panicrecover 是 Go 处理不可恢复的、程序级异常的机制,但它们不应用于常规的错误处理。

7.6.1 panic

panic 会立即中断当前函数的正常执行流程,开始向上回溯(unwind)调用栈,执行沿途的 defer,最终若无人捕获则导致程序崩溃并打印堆栈信息:

func mustPositive(n int) {
    if n < 0 {
        panic("数值不能为负")
    }
}

触发 panic 的常见情况还包括:数组越界、空指针解引用、向 nil map 写入、类型断言失败等。

7.6.2 recover

recover 只能在 defer 函数中调用,用于捕获 panic、恢复程序的正常执行:

func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("发生 panic: %v", r)
        }
    }()
    return a / b, nil // 当 b 为 0 时触发 panic
}

result, err := safeDivide(10, 0)
fmt.Println(result, err) // 0 发生 panic: runtime error: integer divide by zero

这里利用了命名返回值 err,在 defer 中将 panic 转换为普通错误返回。

7.6.3 何时使用 panic

应当极少使用 panic。合理的使用场景包括:

  • 程序启动时的初始化失败(如配置文件缺失、必需的环境变量未设置),此时程序无法继续运行。
  • 不可能发生的逻辑错误(编程错误),表示一个 bug。
  • 库的内部使用 panic,但在公共 API 边界用 recover 转换为 error 返回给调用者。

原则:对于可预期的、调用者应当处理的错误,返回 error;对于不可恢复的、表明程序进入错误状态的情况,才使用 panic。

7.7 错误处理最佳实践

  1. 不要忽略错误:避免使用 _ 丢弃 error,除非你明确知道可以忽略。
  2. 错误只处理一次:要么处理(如记录日志、返回默认值),要么向上传播,不要既记录又返回(会导致重复日志)。
  3. 添加上下文:传播错误时用 %w 说明”在做什么时出错”,便于排查。
  4. 错误信息小写、不带标点结尾:因为错误常被包装拼接,如 "open file failed" 而非 "Open file failed."
  5. 在边界处理 panic:库代码不应让 panic 逃逸到调用者,应在公共 API 用 recover 转换。
  6. 优先使用 errors.Is/As:而非直接 == 比较或类型断言。

7.8 本章小结

本章详细讲解了 Go 独特的错误处理哲学:将错误作为显式返回值处理,而非依赖异常。我们学习了 error 接口、哨兵错误、自定义错误类型,以及 Go 1.13 引入的错误包装机制(%werrors.Iserrors.As)。panic/recover 则用于处理真正不可恢复的异常情况,应谨慎使用。良好的错误处理是编写健壮 Go 程序的核心能力。

下一章我们将进入 Go 的招牌特性——并发编程,学习 goroutine 与 channel。