编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • 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
}
1
2
3

为什么这么简单?

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)
}
1
2
3
4
5
6
7

对比 Java:

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

try {
    byte[] data = readFile("config.json");
} catch (IOException e) {
    // 不写 catch 就默认抛给上层
}
1
2
3
4
5
6
7
8

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
    }
    // ...
}
1
2
3
4
5
6
7
8
9
10

特点:

  • 每次调用返回不同的错误实例(即使字符串相同)
  • 适合创建"哨兵错误"(见 §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)
    }
    // ...
}
1
2
3
4
5
6
7
8
9

特点:

  • 支持格式化,适合带动态信息的错误
  • 性能比 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
1
2
3
4
5
6

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
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

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

# 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 返回的错误没包装)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

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}
    }
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

现在 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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

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


# 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:找到了")  // 走这里!
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

输出:

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

为什么? 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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 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")
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

errors.As 的签名:

func As(err error, target any) bool
1

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

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

var perr *os.PathError  
if errors.As(err, &perr) { ... }   // &perr 是 **os.PathError
1
2
3
4
5

# 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)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

输出:

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

# 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("包含年龄错误")
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 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
    }
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12

优点:

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

缺点:

  • 不能携带额外信息
  • 容易产生命名冲突(不同包的 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,
        }
    }
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

优点:

  • 信息丰富,便于调试
  • 可以定义自定义方法(如 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: "用户不存在"}
    }
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

优点:

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

缺点:

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

适用:库的公共 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 正常结束")  // 不会执行
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

输出:

c 的 defer
main 的 defer
panic: a 崩了

(调用栈)
1
2
3
4
5

传播路径:

  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("程序继续运行")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

输出:

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

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

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

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("程序结束")  // 不会执行(整个进程崩溃)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

每个 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("程序正常结束")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 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
    }
    
    1
    2
    3
    4
    5
    6
    7
  2. 编程错误(bug):

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

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

不该用 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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 为什么这种风格好?

  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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 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
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214

这个例子展示了:

  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) { ... }  // 递归解包匹配
1
2
3
4
5

# 陷阱 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 让调用方处理
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 陷阱 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")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 陷阱 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")
    }()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 陷阱 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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 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"])
    }
    
    1
    2
    3
    4
    5
    6
  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))
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
  8. 工程题:在一个大型项目中,如何统一错误处理风格?请制定 3 条团队规范。

# 11.13 推荐阅读

  • 卷一第 10 章 接口与多态(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/10, 11:13:41
接口与多态
并发goroutine

← 接口与多态 并发goroutine→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式