Skip to content

第08章 错误处理 —— Go 的"反异常"哲学

本章目标:理解 Go 把错误当成普通值的设计、掌握 error 接口、错误包装、自定义错误,以及 panic/recover 的正确使用。


1. Go 的错误观

🌰 生活化比喻:其他语言(如 Java)把错误当成"突发交通事故"——平时不管,撞了再说(异常机制)。Go 把错误当成"路上的限速牌"——每一步都要看,每一步都要决定怎么办。

Errors are values. —— Rob Pike(Go 之父之一)

go
// Java 风格
try {
    file = open("x.txt");
    data = file.read();
} catch (Exception e) { ... }

// Go 风格
file, err := os.Open("x.txt")
if err != nil { return err }
defer file.Close()

data, err := io.ReadAll(file)
if err != nil { return err }

📊 配套图解:diagrams/error-philosophy.svg


2. error 接口:标准库定义

go
type error interface {
    Error() string
}

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

2.1 创建错误

go
import "errors"

err1 := errors.New("文件未找到")
err2 := fmt.Errorf("打开 %s 失败:%v", filename, cause)

2.2 处理错误的标准模式

go
result, err := someFunc()
if err != nil {
    return fmt.Errorf("做某事失败: %w", err)  // %w 是包装
}
// 用 result...

90% 的 Go 错误处理就这一个 if 块。


3. 自定义错误类型

go
type NotFoundError struct {
    Resource string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s 未找到", e.Resource)
}

func GetUser(id string) (*User, error) {
    if !exists(id) {
        return nil, &NotFoundError{Resource: "user/" + id}
    }
    return &User{ID: id}, nil
}

3.1 类型断言识别错误

go
user, err := GetUser("123")
if err != nil {
    if _, ok := err.(*NotFoundError); ok {
        // 专门处理"未找到"
    } else {
        // 其他错误
    }
}

📊 配套图解:diagrams/error-types.svg


4. 错误包装与解包(Go 1.13+)

4.1 用 %w 包装

go
if err := db.Query(...); err != nil {
    return fmt.Errorf("查询用户失败: %w", err)
}

%w(不是 %v)会把原 error "嵌入" 新 error,形成错误链

4.2 errors.Iserrors.As

go
import "errors"

if errors.Is(err, os.ErrNotExist) {
    // 不管被包装多少层,只要"链"里有这个错误就 true
}

var notFound *NotFoundError
if errors.As(err, &notFound) {
    // 把链里的某个特定类型 error 提取出来
    fmt.Println(notFound.Resource)
}

🌰 生活化比喻:errors.Is 像在购物袋里翻找特定商品errors.As 像找到后把它取出来

📊 配套图解:diagrams/error-wrapping.svg


5. 哨兵错误(Sentinel Errors)

go
// 标准库的常见做法
var ErrNotFound = errors.New("not found")

if errors.Is(err, ErrNotFound) {
    // 处理
}

经典例子:

  • io.EOF
  • sql.ErrNoRows
  • os.ErrNotExist

⚠️ 滥用哨兵错误会导致包之间的强耦合,所以现代 Go 倾向于用类型断言或 errors.As


6. panic:撞了南墙再说

go
func divide(a, b int) int {
    if b == 0 {
        panic("除数不能为 0")
    }
    return a / b
}

panic 会:

  1. 立即停止当前函数执行
  2. 执行所有 defer
  3. 向上层函数传递(也叫 unwind)
  4. 一直传到 main 还没被处理就 程序崩溃

🌰 生活化比喻:panic 像警铃——只在程序到了"无法继续运行"的地步才用。

6.1 什么时候用 panic?

✅ 用:

  • 数组越界、空指针解引用(运行时自动 panic)
  • 程序初始化时遇到不可恢复错误(如配置文件损坏)
  • 库函数中违反契约(参数明显错误)

❌ 不用:

  • 业务错误(如"用户不存在")—— 用 error
  • 网络错误、文件读写错误 —— 用 error

7. recover:从 panic 中拯救

go
func safe() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("捕获到 panic:", r)
        }
    }()
    panic("oops")
}

重点

  1. recover 只在 defer 函数内部有效
  2. 一旦 recover 返回非 nil,panic 就被"消化"了,函数正常返回

7.1 实际场景

最常见的 recover 场景是 HTTP 服务的全局兜底

go
func Recovery(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("请求 %s panic: %v", r.URL.Path, err)
                http.Error(w, "服务器内部错误", 500)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

避免一个请求 panic 把整个服务搞挂。

📊 配套图解:diagrams/panic-recover.svg


8. panic 的"传播"过程

go
func a() { b() }
func b() { c() }
func c() { panic("oops") }

func main() {
    defer fmt.Println("main 的 defer")
    a()
}

执行流程:

main → a → b → c → panic!

               unwind

       (没人 recover)

      执行 main 的 defer

            程序退出 + 打印 stack

9. 错误处理最佳实践

9.1 三大原则

  1. 错误信息要"沿调用链增量":每层加一点上下文
  2. 不要丢弃错误_ = doSth() 是反模式
  3. %w 不用 %v:保持错误链可追溯

9.2 反例 vs 正例

go
// ❌ 反例:吃掉错误
data, _ := os.ReadFile(name)

// ❌ 反例:错误信息丢失
return errors.New("失败")

// ✅ 正例:保留链条
return fmt.Errorf("读配置文件 %s 失败: %w", name, err)

10. 案例:完整的错误处理

go
package main

import (
    "errors"
    "fmt"
)

var ErrUserNotFound = errors.New("user not found")

type ValidationError struct {
    Field string
}

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

func GetUser(id string) (string, error) {
    if id == "" {
        return "", &ValidationError{Field: "id"}
    }
    if id == "999" {
        return "", fmt.Errorf("查询用户 %s: %w", id, ErrUserNotFound)
    }
    return "Alice", nil
}

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

    _, err = GetUser("999")
    if errors.Is(err, ErrUserNotFound) {
        fmt.Println("用户不存在(即使被包装了一层)")
    }
}

11. 本章小结

✅ Go 把错误当普通值处理,不用 try-catch
error 是个只有 Error() 方法的接口
✅ 用 errors.Newfmt.Errorf 创建
✅ 用 %w 包装错误形成链
✅ 用 errors.Is / errors.As 检查链
✅ 自定义错误类型,类型断言可专项处理
✅ panic 只在不可恢复错误才用,业务错误用 error
✅ recover 只在 defer 内有效


📂 配套资源