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

推荐订阅源

D
Docker
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cisco Blogs
Scott Helme
Scott Helme
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
C
Cyber Attacks, Cyber Crime and Cyber Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
Schneier on Security
I
Intezer
Spread Privacy
Spread Privacy
AWS News Blog
AWS News Blog
V
Vulnerabilities – Threatpost
Cloudbric
Cloudbric
V2EX - 技术
V2EX - 技术
Google Online Security Blog
Google Online Security Blog
L
Lohrmann on Cybersecurity
Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
LINUX DO - 热门话题
S
Secure Thoughts
T
The Exploit Database - CXSecurity.com
博客园 - 【当耐特】
Recent Announcements
Recent Announcements
Security Archives - TechRepublic
Security Archives - TechRepublic
Stack Overflow Blog
Stack Overflow Blog
罗磊的独立博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
K
Kaspersky official blog
阮一峰的网络日志
阮一峰的网络日志
博客园_首页
Latest news
Latest news
B
Blog
F
Full Disclosure
大猫的无限游戏
大猫的无限游戏
博客园 - 叶小钗
L
LangChain Blog
GbyAI
GbyAI
Last Week in AI
Last Week in AI
S
Security Affairs
Apple Machine Learning Research
Apple Machine Learning Research
N
Netflix TechBlog - Medium
Security Latest
Security Latest
Vercel News
Vercel News
Y
Y Combinator Blog
G
GRAHAM CLULEY
S
Securelist
T
Troy Hunt's Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
雷峰网
雷峰网

博客园 - 我才是银古

第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章:参数化表达式与Spreadsheet 第08章:装配设计Assembly 第06章:Part工作台与几何内核 第05章:PartDesign实体特征建模 第04章:草图Sketcher约束建模 第02章:安装版本与工作环境配置 第03章:界面工作台与基础操作 第01章:项目全景与学习路线 第十二章:插件开发、研究功能与最佳实践 第十章:定时任务与自动化(Cron) 第七章:技能、记忆与自学习闭环 第八章:MCP 集成与上下文文件 第六章:工具系统与终端后端 第五章:模型供应商与配置体系 Hermes Agent 教程目录 第十一章:语音、视觉、浏览器与子代理协作 第四章:CLI/TUI 与会话管理 第十二章:学习路线、实战方案与最佳实践 第十一章:源码结构、开发调试与插件开发 第十章:自动化、远程访问、日志与排障 第九章:Control UI、节点、Canvas 与语音能力 第七章:工具、技能、插件与能力扩展 第八章:安全模型、访问控制与沙箱实践 第六章:Agent 工作区、会话与多智能体路由 第五章:多通道消息接入与聊天平台配置 第四章:配置体系、模型接入与认证管理 第三章:Gateway 架构、协议与运行机制 第二章:安装、环境准备与快速上手 第一章:OpenClaw 项目概览与核心定位 oh-my-openagent 教程目录 09-命令模型回退与配置参考 10-实战案例最佳实践与故障排除 05-工作模式-Ultrawork-Prometheus-Atlas 08-Hooks与MCP系统 06-Category与Skill系统 07-核心工具链 04-智能体全景详解 03-安装与环境配置 02-整体架构与多模型编排机制 01-项目简介与核心理念 01-项目概览与学习路线 02-安装部署与工具适配 03-Skill机制与using-superpowers 05-TDD系统化调试与完成前验证 04-需求澄清方案设计与计划编写 07-并行智能体子智能体与Git-Worktree 第六章:代码审查、反馈处理与分支收尾 08-中国特色Skills与本土团队落地 09-MCP构建工作流执行与自定义Skill 第23章:FreeCAD-Python-API Clipper2 C# 源码解读教程 第19章:PolyTree 多边形树结构 第20章:实际应用与最佳实践 第18章:Minkowski 和与差 第17章:RectClip 矩形裁剪优化 第16章:ClipperOffset 偏移类详解 第15章:填充规则详解 第14章:布尔运算执行流程 第13章:ClipperD 浮点裁剪类 第11章:OutRec 与 OutPt 输出结构 第9章:Active 活动边结构 第10章:Vertex 顶点与 LocalMinima 局部极小值 第12章:Clipper64 裁剪类详解 第7章:高精度运算与128位整数 第8章:ClipperBase 基类详解 第5章:枚举类型与常量定义 第6章:InternalClipper 内部工具类 第2章:核心数据结构 - Point64、PointD 第3章:路径与多边形表示 - Path64、PathD、Paths64、PathsD 第4章:矩形边界 - Rect64、RectD
第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。