编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

    • 入门教程

      • README
      • Go简史
      • 基础语法
      • 数据类型
      • 运算符
      • 复合类型
      • 流程语句
      • 函数
      • 指针与逃逸
      • 结构体与方法
      • 接口与多态
      • 错误处理
        • 目录介绍
        • 11.1 本章学习目标
        • 11.2 error 接口的设计哲学
          • 为什么 Go 不用异常?
        • 11.3 创建错误:errors.New / fmt.Errorf
          • 11.3.1 errors.New
          • 11.3.2 fmt.Errorf
          • 性能对比
          • 什么时候该用哪个?
        • 11.4 错误包装:%w 与 unwrap 链
          • 11.4.1 包装语法:%w
          • 11.4.2 解包:errors.Unwrap
          • 11.4.3 实现自定义包装错误
          • 11.4.4 多层包装实战
        • 11.5 错误判断:errors.Is 与 errors.As
          • 11.5.1 为什么 err == target 不够用?
          • 11.5.2 errors.Is 的递归解包
          • 11.5.3 errors.As:类型断言的安全版
          • 11.5.4 标准库中的 Is / As 应用
        • 11.6 多错误合并:errors.Join(Go 1.20+)
          • errors.Join 的特点
          • 与 %w 包装的区别
        • 11.7 哨兵错误 vs 错误类型 vs 不透明错误
          • 11.7.1 哨兵错误(Sentinel Errors)
          • 11.7.2 错误类型(Error Types)
          • 11.7.3 不透明错误(Opaque Errors)
          • 选择指南
        • 11.8 panic / recover 的边界
          • 11.8.1 panic 的传播规则
          • 11.8.2 recover 必须配 defer
          • 11.8.3 跨 goroutine 不能 recover
          • 11.8.4 何时该用 panic 而非 error
        • 11.9 错误处理三段式与 if err != nil 美学
          • 经典三段式
          • 为什么这种风格好?
          • 处理错误的几种方式
          • 避免的错误处理反模式
        • 11.10 综合示例:构建分层错误体系
        • 11.11 Go 新手陷阱 Top 5
          • 陷阱 1:err == target 比较包装错误
          • 陷阱 2:用 panic 当业务异常
          • 陷阱 3:recover 不在 defer 里
          • 陷阱 4:跨 goroutine 期望 recover
          • 陷阱 5:*T 类型 nil 赋给接口(第 10 章讲过)
        • 11.12 思考题
        • 11.13 推荐阅读
      • 并发goroutine
      • 通道channel
      • 同步sync包
      • IO和文件
      • 标准库与泛型
      • 工程化与模块
      • 特性图谱
    • 综合案例

    • 专栏博客

    • 开发技巧

  • JavaScript入门

  • CodeX
  • Go入门到精通
  • 入门教程
杨充
2026-05-21
目录

错误处理

# 第 11 章 错误处理

Go 的错误处理是"返回值"哲学的极致体现:错误是值,显式传递。 关键词:error 接口、errors.Is / errors.As / %w、errors.Join(1.20+)、panic / recover


# 目录介绍

  • 11.1 本章学习目标
  • 11.2 error 接口的设计哲学
  • 11.3 创建错误:errors.New / fmt.Errorf
  • 11.4 错误包装:%w 与 unwrap 链
  • 11.5 错误判断:errors.Is 与 errors.As
  • 11.6 多错误合并:errors.Join(Go 1.20+)
  • 11.7 哨兵错误 vs 错误类型 vs 不透明错误
  • 11.8 panic / recover 的边界
    • 11.8.1 panic 的传播规则
    • 11.8.2 recover 必须配 defer
    • 11.8.3 跨 goroutine 不能 recover
    • 11.8.4 何时该用 panic 而非 error
  • 11.9 错误处理三段式与 if err != nil 美学
  • 11.10 综合示例:构建分层错误体系
  • 11.11 Go 新手陷阱 Top 5
  • 11.12 思考题
  • 11.13 推荐阅读

# 11.1 本章学习目标

学完本章你应当能够:

  • ✅ 能用 errors.New / fmt.Errorf 创建错误,理解两者的性能差异
  • ✅ 能用 %w 包装错误形成链,并用 errors.Unwrap / errors.Is / errors.As 解包
  • ✅ 能解释为什么 err == io.EOF 不够用,必须用 errors.Is(err, io.EOF)
  • ✅ 能区分哨兵错误、错误类型、不透明错误三种风格,并知道各自适用场景
  • ✅ 能正确使用 errors.Join 合并多个错误(Go 1.20+)
  • ✅ 能画图说明 panic 的传播路径,知道 recover 必须在 defer 里
  • ✅ 能解释"为什么 Go 不用异常",并说出 error vs panic 的边界
  • ✅ 能写出符合 Go 美学的"错误处理三段式"代码

本章是 Go 工程化的第二道分水岭。理解了错误处理,你写的代码就具备了生产级健壮性。


# 11.2 error 接口的设计哲学

回顾第 10 章:error 是一个只有一个方法的接口:

type error interface {
    Error() string
}

为什么这么简单?

Go 团队认为:错误处理的核心问题是信息传递,不是控制流跳转。所以:

语言 错误处理机制 哲学
Java/C#/Python 异常(exception) 控制流跳转,栈展开
Go 错误值(error) 值传递,显式处理

# 为什么 Go 不用异常?

  1. 异常破坏了函数签名:你看不出一个函数会抛什么异常,除非看文档或源码
  2. 异常隐藏了控制流:try/catch 让代码的"正常路径"和"错误路径"混在一起
  3. 异常鼓励忽略错误:不写 catch 就默认向上抛,容易漏处理
  4. 异常性能开销大:栈展开、异常对象构造比返回值开销大

Go 的选择:错误是值,必须显式处理。

// Go:错误是返回值,必须处理
func ReadFile(name string) ([]byte, error)

data, err := ReadFile("config.json")
if err != nil {
    return fmt.Errorf("读取配置失败: %w", err)
}

对比 Java:

// Java:异常是控制流,可以不处理
byte[] readFile(String name) throws IOException

try {
    byte[] data = readFile("config.json");
} catch (IOException e) {
    // 不写 catch 就默认抛给上层
}

Go 的核心思想:强迫程序员在每个可能出错的地方做出决策——是处理、是包装、还是向上返回。


# 11.3 创建错误:errors.New / fmt.Errorf

# 11.3.1 errors.New

最简单的创建方式:

import "errors"

var ErrNotFound = errors.New("资源不存在")

func FindUser(id int) (*User, error) {
    if id <= 0 {
        return nil, ErrNotFound
    }
    // ...
}

特点:

  • 每次调用返回不同的错误实例(即使字符串相同)
  • 适合创建"哨兵错误"(见 §11.7)
  • 性能最好:不涉及格式化

# 11.3.2 fmt.Errorf

需要动态信息时用:

func FindUser(id int) (*User, error) {
    if id <= 0 {
        return nil, fmt.Errorf("无效的用户ID: %d", id)
    }
    if id > 1000000 {
        return nil, fmt.Errorf("用户ID超出范围: %d (最大100万)", id)
    }
    // ...
}

特点:

  • 支持格式化,适合带动态信息的错误
  • 性能比 errors.New 稍差(要解析格式字符串)
  • Go 1.13+ 支持 %w 包装(见 §11.4)

# 性能对比

// benchmark
goos: darwin
goarch: arm64

BenchmarkErrorsNew-8     1000000000   0.30 ns/op
BenchmarkFmtErrorf-8       10000000   150  ns/op

fmt.Errorf 比 errors.New 慢 500 倍。所以在热点路径上尽量用 errors.New 或预定义的哨兵错误。

# 什么时候该用哪个?

场景 推荐方式
静态错误消息(如"文件不存在") errors.New 或预定义哨兵
需要动态信息(如"用户 123 不存在") fmt.Errorf
需要包装底层错误 fmt.Errorf("... %w", err)
性能敏感的热点路径 errors.New 或预定义

# 11.4 错误包装:%w 与 unwrap 链

Go 1.13 引入了错误包装(error wrapping),让错误可以形成链。

# 11.4.1 包装语法:%w

func ReadConfig() error {
    data, err := os.ReadFile("config.json")
    if err != nil {
        return fmt.Errorf("读取配置文件失败: %w", err)  // 包装
    }
    // ...
}

func main() {
    err := ReadConfig()
    if err != nil {
        fmt.Println(err)  // 读取配置文件失败: open config.json: no such file or directory
    }
}

%w 把底层错误 err 包装进新错误里,形成链:

读取配置文件失败: open config.json: no such file or directory
           ↑ 新错误                      ↑ 被包装的底层错误

# 11.4.2 解包:errors.Unwrap

func main() {
    err := ReadConfig()
    
    // 手动解包
    unwrapped := errors.Unwrap(err)
    fmt.Println(unwrapped)  // open config.json: no such file or directory
    
    // 可以连续解包
    if unwrapped != nil {
        fmt.Println(errors.Unwrap(unwrapped))  // nil(os.ReadFile 返回的错误没包装)
    }
}

errors.Unwrap 返回被包装的错误,如果已经是链尾则返回 nil。

# 11.4.3 实现自定义包装错误

任何实现了 Unwrap() error 方法的错误类型都支持包装链:

type ConfigError struct {
    Msg string
    Err error
}

func (e *ConfigError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("配置错误: %s: %v", e.Msg, e.Err)
    }
    return fmt.Sprintf("配置错误: %s", e.Msg)
}

func (e *ConfigError) Unwrap() error {
    return e.Err  // 关键:返回被包装的错误
}

func ReadConfig() error {
    data, err := os.ReadFile("config.json")
    if err != nil {
        return &ConfigError{Msg: "读取失败", Err: err}
    }
    // ...
}

现在 ConfigError 也支持 errors.Unwrap 和 errors.Is / errors.As 了。

# 11.4.4 多层包装实战

func processUser(id int) error {
    user, err := getUserFromDB(id)
    if err != nil {
        return fmt.Errorf("处理用户 %d 失败: %w", id, err)
    }
    
    if err := validateUser(user); err != nil {
        return fmt.Errorf("用户 %d 验证失败: %w", id, err)
    }
    
    return nil
}

func getUserFromDB(id int) (*User, error) {
    // 模拟数据库错误
    return nil, fmt.Errorf("数据库查询失败: %w", sql.ErrNoRows)
}

func main() {
    err := processUser(123)
    fmt.Println(err)
    // 输出:处理用户 123 失败: 数据库查询失败: sql: no rows in result set
    
    // 解包链
    fmt.Println(errors.Unwrap(err))                    // 数据库查询失败: sql: no rows in result set
    fmt.Println(errors.Unwrap(errors.Unwrap(err)))     // sql: no rows in result set
    fmt.Println(errors.Unwrap(errors.Unwrap(errors.Unwrap(err))))  // nil
}

包装链的价值:保留了完整的错误上下文,同时让底层错误仍然可识别。


# 11.5 错误判断:errors.Is 与 errors.As

# 11.5.1 为什么 err == target 不够用?

看这个例子:

var ErrNotFound = errors.New("not found")

func find() error {
    return fmt.Errorf("查询失败: %w", ErrNotFound)
}

func main() {
    err := find()
    
    // ❌ 错误的方式
    if err == ErrNotFound {
        fmt.Println("直接比较:找到了")
    } else {
        fmt.Println("直接比较:没找到")  // 走这里!
    }
    
    // ✅ 正确的方式
    if errors.Is(err, ErrNotFound) {
        fmt.Println("errors.Is:找到了")  // 走这里!
    }
}

输出:

直接比较:没找到
errors.Is:找到了

为什么? err 是包装错误,ErrNotFound 在链的深处。== 只比较最外层,errors.Is 会递归解包整个链。

# 11.5.2 errors.Is 的递归解包

errors.Is(err, target) 的逻辑:

  1. 如果 err == target,返回 true
  2. 如果 err 实现了 Unwrap() error,对 Unwrap() 的结果递归调用 errors.Is
  3. 如果 err 实现了 Is(error) bool 方法,调用 err.Is(target)

第 3 步允许自定义相等逻辑:

type MyError struct {
    Code int
    Msg  string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("错误 %d: %s", e.Code, e.Msg)
}

// 自定义相等逻辑:只要 Code 相同就认为相等
func (e *MyError) Is(target error) bool {
    if t, ok := target.(*MyError); ok {
        return e.Code == t.Code
    }
    return false
}

func main() {
    err1 := &MyError{Code: 404, Msg: "页面1不存在"}
    err2 := &MyError{Code: 404, Msg: "页面2不存在"}  // 相同 Code,不同 Msg
    
    wrapped := fmt.Errorf("包装: %w", err1)
    
    fmt.Println(errors.Is(wrapped, err2))  // true!因为 Code 都是 404
}

# 11.5.3 errors.As:类型断言的安全版

errors.As 是类型断言 i.(T) 的安全版,支持递归解包:

type DBError struct {
    Query string
    Err   error
}

func (e *DBError) Error() string {
    return fmt.Sprintf("数据库错误[%s]: %v", e.Query, e.Err)
}

func (e *DBError) Unwrap() error {
    return e.Err
}

func process() error {
    return fmt.Errorf("业务层: %w", 
        &DBError{Query: "SELECT * FROM users", Err: sql.ErrNoRows})
}

func main() {
    err := process()
    
    // ❌ 不安全的方式(可能 panic)
    // dbErr := err.(*DBError)  // panic: 外层不是 *DBError
    
    // ✅ 安全的方式
    var dbErr *DBError
    if errors.As(err, &dbErr) {  // 注意:第二个参数是指针的指针
        fmt.Printf("捕获到 DBError: %v\n", dbErr.Query)  // SELECT * FROM users
    }
    
    // 也能捕获链深处的 sql.ErrNoRows
    if errors.Is(err, sql.ErrNoRows) {
        fmt.Println("底层是 sql.ErrNoRows")
    }
}

errors.As 的签名:

func As(err error, target any) bool

target 必须是指向接口或具体类型的指针的指针。常见模式:

var dbErr *DBError
if errors.As(err, &dbErr) { ... }  // &dbErr 是 **DBError

var perr *os.PathError  
if errors.As(err, &perr) { ... }   // &perr 是 **os.PathError

# 11.5.4 标准库中的 Is / As 应用

os 包大量使用:

func main() {
    _, err := os.Open("/nonexistent")
    
    // 判断是不是"文件不存在"错误
    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("文件不存在")
    }
    
    // 提取 PathError 的详细信息
    var pathErr *os.PathError
    if errors.As(err, &pathErr) {
        fmt.Printf("操作: %s, 路径: %s, 错误: %v\n", 
            pathErr.Op, pathErr.Path, pathErr.Err)
    }
}

输出:

文件不存在
操作: open, 路径: /nonexistent, 错误: no such file or directory

# 11.6 多错误合并:errors.Join(Go 1.20+)

Go 1.20 引入了 errors.Join,用于合并多个错误:

func validateUser(u *User) error {
    var errs []error
    
    if u.Name == "" {
        errs = append(errs, errors.New("姓名不能为空"))
    }
    if u.Age < 0 {
        errs = append(errs, errors.New("年龄不能为负"))
    }
    if len(u.Email) < 5 {
        errs = append(errs, errors.New("邮箱格式不正确"))
    }
    
    if len(errs) > 0 {
        return errors.Join(errs...)  // 合并所有错误
    }
    return nil
}

func main() {
    u := &User{Name: "", Age: -1, Email: "a@b"}
    err := validateUser(u)
    
    if err != nil {
        fmt.Println(err)
        // 输出:
        // 姓名不能为空
        // 年龄不能为负  
        // 邮箱格式不正确
    }
    
    // 也可以用 errors.Is 检查合并错误里是否包含特定错误
    if errors.Is(err, errors.New("年龄不能为负")) {
        fmt.Println("包含年龄错误")
    }
}

# errors.Join 的特点

  1. 如果所有错误都是 nil,返回 nil
  2. 如果只有一个非 nil 错误,返回那个错误(不包装)
  3. 多个错误时,用换行符连接它们的 Error()
  4. 支持 errors.Is / errors.As:会对每个子错误递归检查

# 与 %w 包装的区别

特性 %w 包装 errors.Join 合并
结构 链式(父子) 平行(兄弟)
Error() 输出 单行,冒号分隔 多行,换行分隔
适用场景 错误有因果关系 错误是并列关系
例子 "读取失败: 文件不存在" "错误1\n错误2\n错误3"

# 11.7 哨兵错误 vs 错误类型 vs 不透明错误

这是 Go 错误处理的三种主要风格。

# 11.7.1 哨兵错误(Sentinel Errors)

定义:预定义的错误变量,用于表示特定状态。

var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized") 
    ErrTimeout      = errors.New("timeout")
)

func FindUser(id int) (*User, error) {
    if id == 0 {
        return nil, ErrNotFound
    }
    // ...
}

优点:

  • 简单直观
  • 性能好(比较的是指针)

缺点:

  • 不能携带额外信息
  • 容易产生命名冲突(不同包的 ErrNotFound 可能含义不同)

适用:标准库的 io.EOF、sql.ErrNoRows 等。

# 11.7.2 错误类型(Error Types)

定义:自定义结构体实现 error 接口,可以携带丰富信息。

type ValidationError struct {
    Field   string
    Message string
    Value   any
}

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

func validateUser(u *User) error {
    if u.Age < 0 {
        return &ValidationError{
            Field: "age", 
            Message: "不能为负数", 
            Value: u.Age,
        }
    }
    // ...
}

优点:

  • 信息丰富,便于调试
  • 可以定义自定义方法(如 ValidationError.Fields())

缺点:

  • 调用方必须知道具体类型才能提取信息(用 errors.As)
  • 增加了包之间的耦合

适用:领域特定的错误,需要携带结构化信息。

# 11.7.3 不透明错误(Opaque Errors)

定义:只告诉调用方"出错了",但不暴露错误细节。

// 包内定义丰富的错误类型
type internalError struct {
    code    int
    message string
    details map[string]any
}

func (e *internalError) Error() string {
    return e.message
}

// 对外只暴露简单的错误
type Database interface {
    FindUser(id int) (*User, error)  // 不暴露具体错误类型
}

// 实现
type mysqlDB struct{}

func (db *mysqlDB) FindUser(id int) (*User, error) {
    if 出错 {
        // 内部用丰富错误
        return nil, &internalError{code: 404, message: "用户不存在"}
    }
    // ...
}

优点:

  • 封装性好,不暴露实现细节
  • 便于重构(内部错误类型可以随便改)

缺点:

  • 调用方能做的有限(只能记录或向上传递)

适用:库的公共 API,不希望调用方依赖错误细节。

# 选择指南

场景 推荐风格 例子
简单状态标志 哨兵错误 io.EOF、sql.ErrNoRows
需要丰富调试信息 错误类型 os.PathError、json.SyntaxError
公共 API,隐藏实现 不透明错误 http.Client.Do 返回的 error
业务验证错误 错误类型 ValidationError 带字段信息

# 11.8 panic / recover 的边界

panic 是 Go 的"真正异常",但使用边界非常严格。

# 11.8.1 panic 的传播规则

func a() {
    panic("a 崩了")
}

func b() {
    a()
    fmt.Println("b 正常结束")  // 不会执行
}

func c() {
    defer fmt.Println("c 的 defer")
    b()
    fmt.Println("c 正常结束")  // 不会执行
}

func main() {
    defer fmt.Println("main 的 defer")
    c()
    fmt.Println("main 正常结束")  // 不会执行
}

输出:

c 的 defer
main 的 defer
panic: a 崩了

(调用栈)

传播路径:

  1. a() panic
  2. a() 的 defer 执行(本例没有)
  3. 跳到 b(),b() 的 defer 执行(本例没有)
  4. 跳到 c(),c() 的 defer 执行
  5. 跳到 main(),main() 的 defer 执行
  6. 程序崩溃,打印调用栈

关键规则:panic 会沿着调用栈向上传播,执行沿途的 defer,直到被 recover 或程序退出。

# 11.8.2 recover 必须配 defer

recover 只能在 defer 里生效:

func safeCall() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("捕获到 panic:", r)
        }
    }()
    
    panic("测试 panic")
    fmt.Println("这行不会执行")
}

func main() {
    safeCall()
    fmt.Println("程序继续运行")
}

输出:

捕获到 panic: 测试 panic
程序继续运行

如果把 recover 放在普通代码里:

func wrong() {
    if r := recover(); r != nil {  // ❌ 这里不会生效
        fmt.Println("捕获到:", r)
    }
    panic("测试")
}

recover 只在当前函数的 defer 里调用时才有效。

# 11.8.3 跨 goroutine 不能 recover

func main() {
    go func() {
        panic("goroutine 崩了")
    }()
    
    // 这个 recover 抓不到上面的 panic
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("main 捕获:", r)  // 不会执行
        }
    }()
    
    time.Sleep(time.Second)
    fmt.Println("程序结束")  // 不会执行(整个进程崩溃)
}

每个 goroutine 的 panic 是独立的。一个 goroutine 的 recover 抓不到另一个 goroutine 的 panic。

正确做法:在每个 goroutine 入口处加 recover:

func safeGo(fn func()) {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                log.Printf("goroutine panic: %v", r)
            }
        }()
        fn()
    }()
}

func main() {
    safeGo(func() {
        panic("这个会被捕获")
    })
    
    time.Sleep(time.Second)
    fmt.Println("程序正常结束")
}

# 11.8.4 何时该用 panic 而非 error

该用 panic 的场景:

  1. 真正的不可恢复错误:

    func MustReadFile(name string) []byte {
        data, err := os.ReadFile(name)
        if err != nil {
            panic(fmt.Sprintf("读取关键文件 %s 失败: %v", name, err))
        }
        return data
    }
    
  2. 编程错误(bug):

    func (s *Stack) Pop() int {
        if s.isEmpty() {
            panic("栈为空时调用 Pop")  // 这是调用者的 bug
        }
        // ...
    }
    
  3. 初始化失败:

    func init() {
        if os.Getenv("DB_URL") == "" {
            panic("DB_URL 环境变量未设置")
        }
    }
    

不该用 panic 的场景:

  1. 预期的业务错误(如"用户不存在")→ 用 error
  2. 外部依赖失败(如"数据库连接超时")→ 用 error
  3. 用户输入错误 → 用 error

简单规则:如果错误是调用者能合理预期并处理的,用 error;如果是程序员的错误或系统不可用,用 panic。


# 11.9 错误处理三段式与 if err != nil 美学

Go 的错误处理形成了独特的代码风格——"三段式"。

# 经典三段式

func process() error {
    // 第一段:获取资源
    file, err := os.Open("data.txt")
    if err != nil {
        return fmt.Errorf("打开文件失败: %w", err)
    }
    defer file.Close()  // 确保资源释放
    
    // 第二段:处理数据
    data, err := io.ReadAll(file)
    if err != nil {
        return fmt.Errorf("读取文件失败: %w", err)
    }
    
    var config Config
    if err := json.Unmarshal(data, &config); err != nil {
        return fmt.Errorf("解析JSON失败: %w", err)
    }
    
    // 第三段:业务逻辑(通常不再有错误返回)
    result := doBusiness(config)
    fmt.Println(result)
    
    return nil
}

# 为什么这种风格好?

  1. 错误立即处理:每个可能出错的操作后紧跟错误检查
  2. 资源管理清晰:defer 确保资源释放,即使中间出错
  3. 正常路径突出:所有错误处理在函数开头,主逻辑在后面
  4. 上下文保留:每层错误都包装上下文信息

# 处理错误的几种方式

根据调用方的需求,错误处理有不同策略:

策略 代码示例 适用场景
直接返回 if err != nil { return err } 简单传递,不加上下文
包装返回 if err != nil { return fmt.Errorf("... %w", err) } 添加上下文,保留链
记录日志 if err != nil { log.Printf("..."); return err } 需要记录但不想中断链
降级处理 if err != nil { fallback(); return nil } 错误可忽略,有备选方案
转换错误 if err != nil { return &MyError{...} } 统一错误类型

# 避免的错误处理反模式

// ❌ 反模式1:忽略错误
file, _ := os.Open("data.txt")  // 错误被静默忽略

// ❌ 反模式2:只打印不处理
if err != nil {
    fmt.Println(err)  // 错误被"处理"了,但调用方不知道
    return nil        // 假装成功
}

// ❌ 反模式3:过度包装
if err != nil {
    return fmt.Errorf("函数A: 函数B: 函数C: %w", err)  // 信息冗余
}

// ✅ 正模式:适度包装,有价值的信息
if err != nil {
    return fmt.Errorf("处理用户 %d 配置: %w", userID, err)
}

# 11.10 综合示例:构建分层错误体系

下面用本章知识构建一个完整的分层错误处理体系:

package main

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

// ===== 1. 定义错误体系 =====

// 哨兵错误
var (
    ErrUserNotFound = errors.New("用户不存在")
    ErrInvalidInput = errors.New("输入无效")
)

// 错误类型
type ValidationError struct {
    Field   string
    Message string
    Value   any
}

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

func (e *ValidationError) Unwrap() error {
    return ErrInvalidInput  // 包装哨兵错误
}

type DBError struct {
    Query string
    Op    string
    Err   error
}

func (e *DBError) Error() string {
    return fmt.Sprintf("数据库错误[%s %s]: %v", e.Op, e.Query, e.Err)
}

func (e *DBError) Unwrap() error {
    return e.Err
}

// 业务错误(不透明错误的变体)
type ServiceError struct {
    Code    int
    Message string
    Cause   error
}

func (e *ServiceError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("服务错误 %d: %s (原因: %v)", e.Code, e.Message, e.Cause)
    }
    return fmt.Sprintf("服务错误 %d: %s", e.Code, e.Message)
}

func (e *ServiceError) Unwrap() error {
    return e.Cause
}

// 自定义 Is 方法:按错误码匹配
func (e *ServiceError) Is(target error) bool {
    if t, ok := target.(*ServiceError); ok {
        return e.Code == t.Code
    }
    return false
}

// ===== 2. 业务函数 =====

func validateUserAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field:   "age", 
            Message: "不能为负数", 
            Value:   age,
        }
    }
    if age > 150 {
        return &ValidationError{
            Field:   "age", 
            Message: "超出合理范围", 
            Value:   age,
        }
    }
    return nil
}

func findUserInDB(id int) (*User, error) {
    if id == 999 {  // 模拟用户不存在
        return nil, &DBError{
            Query: "SELECT * FROM users WHERE id = ?",
            Op:    "query",
            Err:   ErrUserNotFound,  // 包装哨兵错误
        }
    }
    if id < 0 {  // 模拟数据库错误
        return nil, &DBError{
            Query: "SELECT * FROM users WHERE id = ?", 
            Op:    "query",
            Err:   fmt.Errorf("数据库连接失败: %w", io.EOF),
        }
    }
    return &User{ID: id, Name: "测试用户"}, nil
}

func getUserProfile(id int) (*User, error) {
    // 输入验证
    if id <= 0 {
        return nil, &ServiceError{
            Code:    400, 
            Message: "用户ID必须为正数",
            Cause:   ErrInvalidInput,
        }
    }
    
    // 业务验证
    if err := validateUserAge(25); err != nil {
        return nil, &ServiceError{
            Code:    422,
            Message: "用户数据验证失败",
            Cause:   err,
        }
    }
    
    // 数据获取
    user, err := findUserInDB(id)
    if err != nil {
        return nil, &ServiceError{
            Code:    500,
            Message: "获取用户信息失败",
            Cause:   err,
        }
    }
    
    return user, nil
}

// ===== 3. 错误处理函数 =====

func handleError(err error) {
    fmt.Printf("原始错误: %v\n", err)
    
    // 检查特定错误类型
    var valErr *ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("验证错误 - 字段: %s, 消息: %s\n", valErr.Field, valErr.Message)
    }
    
    var dbErr *DBError
    if errors.As(err, &dbErr) {
        fmt.Printf("数据库错误 - 操作: %s, 查询: %s\n", dbErr.Op, dbErr.Query)
    }
    
    // 检查特定错误值
    if errors.Is(err, ErrUserNotFound) {
        fmt.Println("错误原因: 用户不存在")
    }
    
    if errors.Is(err, io.EOF) {
        fmt.Println("错误原因: 连接中断(EOF)")
    }
    
    // 检查服务错误码
    var svcErr *ServiceError
    if errors.As(err, &svcErr) {
        fmt.Printf("服务错误码: %d\n", svcErr.Code)
        
        // 按错误码处理
        switch svcErr.Code {
        case 400:
            fmt.Println("处理: 客户端输入错误")
        case 422:
            fmt.Println("处理: 业务验证失败")
        case 500:
            fmt.Println("处理: 服务器内部错误")
        }
    }
    
    fmt.Println("---")
}

// ===== 4. 演示 =====

type User struct {
    ID   int
    Name string
}

func main() {
    fmt.Println("=== 测试1: 输入验证错误 ===")
    _, err1 := getUserProfile(-1)
    handleError(err1)
    
    fmt.Println("=== 测试2: 用户不存在 ===")
    _, err2 := getUserProfile(999)
    handleError(err2)
    
    fmt.Println("=== 测试3: 数据库连接错误 ===")
    _, err3 := getUserProfile(-999)
    handleError(err3)
    
    fmt.Println("=== 测试4: 解包链演示 ===")
    if err3 != nil {
        fmt.Println("第1层:", err3)
        fmt.Println("第2层:", errors.Unwrap(err3))
        fmt.Println("第3层:", errors.Unwrap(errors.Unwrap(err3)))
        fmt.Println("第4层:", errors.Unwrap(errors.Unwrap(errors.Unwrap(err3))))
    }
}

这个例子展示了:

  1. 分层错误体系:哨兵 → 验证错误 → 数据库错误 → 服务错误
  2. 错误包装链:每层都保留底层错误信息
  3. 灵活的错误检查:用 errors.Is 检查值,用 errors.As 提取信息
  4. 自定义匹配逻辑:ServiceError.Is 按错误码匹配

# 11.11 Go 新手陷阱 Top 5

# 陷阱 1:err == target 比较包装错误

// ❌
if err == io.EOF { ... }  // 包装链下永远不匹配

// ✅  
if errors.Is(err, io.EOF) { ... }  // 递归解包匹配

# 陷阱 2:用 panic 当业务异常

// ❌
func FindUser(id int) *User {
    user, err := db.Find(id)
    if err != nil {
        panic(err)  // 业务错误不该 panic
    }
    return user
}

// ✅
func FindUser(id int) (*User, error) {
    return db.Find(id)  // 返回 error 让调用方处理
}

# 陷阱 3:recover 不在 defer 里

// ❌
func bad() {
    if r := recover(); r != nil {  // 不会生效
        fmt.Println(r)
    }
    panic("test")
}

// ✅
func good() {
    defer func() {
        if r := recover(); r != nil {  // 在 defer 里才有效
            fmt.Println(r)
        }
    }()
    panic("test")
}

# 陷阱 4:跨 goroutine 期望 recover

// ❌
func main() {
    go func() {
        panic("goroutine panic")
    }()
    
    defer func() {
        recover()  // 抓不到另一个 goroutine 的 panic
    }()
}

// ✅
func main() {
    go func() {
        defer func() {
            if r := recover(); r != nil {  // 每个 goroutine 自己 recover
                log.Println(r)
            }
        }()
        panic("goroutine panic")
    }()
}

# 陷阱 5:*T 类型 nil 赋给接口(第 10 章讲过)

// ❌
func mayFail() error {
    var p *MyError = nil
    return p  // 接口非 nil!
}

// ✅
func mayFail() error {
    var p *MyError = nil
    if p == nil {
        return nil  // 直接返回 nil 接口
    }
    return p
}

# 11.12 思考题

  1. 解释题:为什么 Go 选择"错误是值"而不是异常?这种设计有什么优缺点?
  2. 代码题:写一个函数,递归打印错误链的所有层级(用 errors.Unwrap)。
  3. 设计题:设计一个文件上传的错误体系,要区分"文件太大"、"格式不支持"、"网络超时"等情况。
  4. 对比题:errors.Is 和 errors.As 有什么区别?各在什么场景下使用?
  5. 实战题:改造以下代码,使其符合 Go 错误处理最佳实践:
    func process() {
        data, _ := ioutil.ReadFile("config.json")
        var config map[string]any
        json.Unmarshal(data, &config)
        fmt.Println(config["version"])
    }
    
  6. 原理题:画图说明 panic 的传播路径,并解释为什么 recover 必须在 defer 里。
  7. 陷阱题:以下代码输出什么?为什么?
    func test() error {
        return fmt.Errorf("外层: %w", io.EOF)
    }
    func main() {
        err := test()
        fmt.Println(err == io.EOF)
        fmt.Println(errors.Is(err, io.EOF))
    }
    
  8. 工程题:在一个大型项目中,如何统一错误处理风格?请制定 3 条团队规范。

# 11.13 推荐阅读

  • 卷一第 10 章 接口与多态 (opens new window)(error 接口基础)
  • 卷三第 14 章 错误与 panic 机制
  • 卷四第 2 章 panic 与 recover 全景图
  • Go Blog: Working with Errors in Go 1.13 (opens new window)
  • Don't just check errors, handle them gracefully - Dave Cheney (opens new window)
  • Error handling and Go - Go Blog (opens new window)

下一章预告:第 12 章《并发 goroutine》——Go 的并发模型核心:轻量级线程 goroutine。我们将学习如何启动成千上万个 goroutine,以及如何用 channel 让它们通信。

上次更新: 2026/06/28, 17:55:19
接口与多态
并发goroutine

← 接口与多态 并发goroutine→

最近更新
01
科学方法实践论法
06-28
02
辩证思维矛盾论法
06-28
03
毛选中的调查观念
06-28
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式