惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
阮一峰的网络日志
阮一峰的网络日志
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
博客园 - 叶小钗
Hugging Face - Blog
Hugging Face - Blog
博客园 - 司徒正美
Last Week in AI
Last Week in AI
罗磊的独立博客
量子位
Jina AI
Jina AI
T
Tailwind CSS Blog
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
美团技术团队
雷峰网
雷峰网
爱范儿
爱范儿
S
SegmentFault 最新的问题
小众软件
小众软件
月光博客
月光博客
酷 壳 – CoolShell
酷 壳 – CoolShell
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报

博客园 - 我才是银古

第16章:常见问题、排错与最佳实践 第15章:扩展生态、MCAD 与外部集成 第12章:实战案例:机械结构与 3D 打印零件 第14章:构建、测试、调试与贡献流程 第13章:OpenSCAD 源码架构与核心执行流程 第11章:预览、渲染、网格精度与性能优化 第09章:列表推导、递归与算法建模 第08章:参数化零件库与复用设计 第10章:导入导出、命令行与自动化 第06章:CSG 布尔建模方法 第07章:二维图形、拉伸、旋转与投影 第05章:基础几何、坐标系与变换 第04章:参数、变量、函数、模块与作用域 OpenSCAD 教程目录 第03章:OpenSCAD 语言基础 第02章:安装、环境配置与开发工作流 第01章:OpenSCAD 项目全景与学习路线 第02章:源码获取、编译与开发环境配置 第01章:OCCT项目全景与学习路线 第18章:二次开发实战与综合案例 第18章:综合实战案例 第17章:数据交换与协同 第16章:源码架构与二次开发 第15章:插件与自定义工作台开发 第14章:Python脚本宏与自动化 第13章:FEM仿真分析 第12章:CAM数控加工 第11章:SurfaceMesh与逆向工程 第10章:Draft二维绘图与BIM建筑 第09章:工程图TechDraw
第07章 - 错误处理机制
我才是银古 · 2026-06-19 · via 博客园 - 我才是银古

第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。