主题
第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 }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.Is 和 errors.As
go
import "errors"
if errors.Is(err, os.ErrNotExist) {
// 不管被包装多少层,只要"链"里有这个错误就 true
}
var notFound *NotFoundError
if errors.As(err, ¬Found) {
// 把链里的某个特定类型 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.EOFsql.ErrNoRowsos.ErrNotExist
⚠️ 滥用哨兵错误会导致包之间的强耦合,所以现代 Go 倾向于用类型断言或 errors.As。
6. panic:撞了南墙再说
go
func divide(a, b int) int {
if b == 0 {
panic("除数不能为 0")
}
return a / b
}panic 会:
- 立即停止当前函数执行
- 执行所有 defer
- 向上层函数传递(也叫 unwind)
- 一直传到
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")
}重点:
recover只在defer函数内部有效- 一旦
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
↓
程序退出 + 打印 stack9. 错误处理最佳实践
9.1 三大原则
- 错误信息要"沿调用链增量":每层加一点上下文
- 不要丢弃错误:
_ = doSth()是反模式 - 用
%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.New、fmt.Errorf 创建
✅ 用 %w 包装错误形成链
✅ 用 errors.Is / errors.As 检查链
✅ 自定义错误类型,类型断言可专项处理
✅ panic 只在不可恢复错误才用,业务错误用 error
✅ recover 只在 defer 内有效