Go 异常处理机

在大多数编程语言(如 Java、Python)中,异常(Exception)是处理错误的核心机制。但 Go 语言刻意不提供传统异常体系,而是构建了一套更加显式、可控的错误处理模型。

本文将从设计思想、核心机制、工程实践三个层面,系统讲解 Go 的“异常处理机制”。


一、设计哲学:Go 为什么不用异常?

Go 的设计目标之一是提高代码可读性与可维护性,因此它做了一个非常关键的取舍:

❗ 用“显式返回错误”替代“隐式异常传播”

传统异常机制的问题:

  • 控制流不清晰(函数可能随时抛异常)
  • 容易遗漏处理(尤其是运行时异常)
  • 性能存在额外开销(栈展开)

Go 的解决方案:

  • 所有错误必须显式返回
  • 调用方必须主动处理
  • 控制流完全可见

二、error:Go 的核心错误处理机制

2.1 基本用法

Go 中的错误是一个普通返回值:

1
2
3
4
5
6
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}

调用方式:

1
2
3
4
5
6
res, err := Divide(10, 0)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(res)

👉 关键点:

  • error != nil 表示发生错误
  • 必须显式判断

2.2 error 的本质

error 是一个接口类型:

1
2
3
type error interface {
Error() string
}

因此你可以自定义错误:

1
2
3
4
5
6
7
8
type BizError struct {
Code int
Msg string
}

func (e *BizError) Error() string {
return fmt.Sprintf("code=%d, msg=%s", e.Code, e.Msg)
}

2.3 错误包装(Go 1.13+)

Go 提供了错误链机制,用于增强上下文信息:

1
2
3
if err != nil {
return fmt.Errorf("read config failed: %w", err)
}

配套方法:

1
2
errors.Is(err, target)
errors.As(err, &targetType)

👉 作用:

  • 保留原始错误
  • 支持逐层解析错误类型

2.4 常见写法模式

模式一:逐层返回

1
2
3
4
5
6
7
8
9
10
func dao() error {
return errors.New("db error")
}

func service() error {
if err := dao(); err != nil {
return err
}
return nil
}

模式二:增强上下文(推荐)

1
2
3
4
5
6
func service() error {
if err := dao(); err != nil {
return fmt.Errorf("service layer failed: %w", err)
}
return nil
}

三、panic:程序级错误

3.1 基本概念

panic 表示程序发生不可恢复错误

1
panic("something went wrong")

执行过程:

  1. 立即停止当前函数
  2. 依次执行 defer
  3. 向上回溯调用栈
  4. 最终导致程序崩溃(若未恢复)

3.2 常见触发场景

  • 数组越界
  • nil 指针解引用
  • 并发错误
1
2
var p *int
fmt.Println(*p) // panic

3.3 使用原则

❌ 不推荐:

1
2
3
if err != nil {
panic(err)
}

✔ 推荐:

  • 仅用于程序逻辑错误
  • 不可恢复的异常状态

四、recover:捕获 panic

4.1 基本用法

recover 用于拦截 panic,必须在 defer 中使用:

1
2
3
4
5
6
7
8
9
func safeRun() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()

panic("error occurred")
}

4.2 特点

  • 只能在 defer 中生效
  • 只对当前 goroutine 有效

4.3 实际应用场景

Web 服务容错

1
2
3
4
5
6
7
8
9
10
11
func RecoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Println("panic:", err)
http.Error(w, "internal error", 500)
}
}()
next.ServeHTTP(w, r)
})
}

👉 防止单个请求导致服务崩溃

goroutine 保护

1
2
3
4
5
6
7
8
9
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("goroutine panic:", r)
}
}()

panic("boom")
}()

五、三种机制对比

机制 用途 推荐程度 说明
error 业务错误 ⭐⭐⭐⭐⭐ 主流方式
panic 致命错误 ⭐⭐ 不可恢复
recover 异常兜底 ⭐⭐⭐ 系统保护

六、工程实践(重点)

6.1 分层使用原则

业务层

  • 使用 error
  • 禁止 panic

框架层(入口)

  • 使用 recover 统一兜底

底层库

  • 可在严重错误时使用 panic

6.2 推荐错误处理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
func handler() {
if err := service(); err != nil {
log.Println(err)
return
}
}

func service() error {
if err := dao(); err != nil {
return fmt.Errorf("service failed: %w", err)
}
return nil
}

👉 特点:

  • 错误逐层返回
  • 每层补充上下文
  • 最终统一处理

6.3 常见反模式

❌ 忽略错误

1
res, _ := Divide(10, 0)

❌ 滥用 panic

1
2
3
if err != nil {
panic(err)
}

❌ 不包装错误

1
return err

👉 问题:缺乏上下文


七、与传统异常机制对比

维度 Go Java
错误传递 返回值 throw
控制流 显式 隐式
强制处理 手动 编译期(部分)
性能 稳定 有额外开销
可读性 冗长但清晰 简洁但隐藏逻辑

八、总结

机制 作用 适用场景 Go 语言惯例
error 显式返回,表示函数执行结果中的预期错误 文件未找到、网络连接失败、输入参数无效等可预测和可恢复的错误 推荐:大部分情况下使用 error 接口进行错误处理
panic/recover 抛出/捕获异常,处理程序中不可恢复的运行时错误 数组越界、空指针引用、关键服务初始化失败等不可恢复的错误 仅用于处理真正的程序异常/崩溃,或在底层库中将 panic 转换为 error

一句话总结:
Go 的错误处理 = 显式 error + 限制性 panic + recover 兜底

这种设计使得程序行为更加可预测,非常适合构建高可靠的后端系统。