错误处理
# 第 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.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 不用异常",并说出
errorvspanic的边界 - ✅ 能写出符合 Go 美学的"错误处理三段式"代码
本章是 Go 工程化的第二道分水岭。理解了错误处理,你写的代码就具备了生产级健壮性。
# 11.2 error 接口的设计哲学
回顾第 10 章:error 是一个只有一个方法的接口:
type error interface {
Error() string
}
为什么这么简单?
Go 团队认为:错误处理的核心问题是信息传递,不是控制流跳转。所以:
| 语言 | 错误处理机制 | 哲学 |
|---|---|---|
| Java/C#/Python | 异常(exception) | 控制流跳转,栈展开 |
| Go | 错误值(error) | 值传递,显式处理 |
# 为什么 Go 不用异常?
- 异常破坏了函数签名:你看不出一个函数会抛什么异常,除非看文档或源码
- 异常隐藏了控制流:
try/catch让代码的"正常路径"和"错误路径"混在一起 - 异常鼓励忽略错误:不写
catch就默认向上抛,容易漏处理 - 异常性能开销大:栈展开、异常对象构造比返回值开销大
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) 的逻辑:
- 如果
err == target,返回true - 如果
err实现了Unwrap() error,对Unwrap()的结果递归调用errors.Is - 如果
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 的特点
- 如果所有错误都是
nil,返回nil - 如果只有一个非
nil错误,返回那个错误(不包装) - 多个错误时,用换行符连接它们的
Error() - 支持
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 崩了
(调用栈)
传播路径:
a()panica()的 defer 执行(本例没有)- 跳到
b(),b()的 defer 执行(本例没有) - 跳到
c(),c()的 defer 执行 - 跳到
main(),main()的 defer 执行 - 程序崩溃,打印调用栈
关键规则: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 的场景:
真正的不可恢复错误:
func MustReadFile(name string) []byte { data, err := os.ReadFile(name) if err != nil { panic(fmt.Sprintf("读取关键文件 %s 失败: %v", name, err)) } return data }编程错误(bug):
func (s *Stack) Pop() int { if s.isEmpty() { panic("栈为空时调用 Pop") // 这是调用者的 bug } // ... }初始化失败:
func init() { if os.Getenv("DB_URL") == "" { panic("DB_URL 环境变量未设置") } }
不该用 panic 的场景:
- 预期的业务错误(如"用户不存在")→ 用
error - 外部依赖失败(如"数据库连接超时")→ 用
error - 用户输入错误 → 用
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
}
# 为什么这种风格好?
- 错误立即处理:每个可能出错的操作后紧跟错误检查
- 资源管理清晰:
defer确保资源释放,即使中间出错 - 正常路径突出:所有错误处理在函数开头,主逻辑在后面
- 上下文保留:每层错误都包装上下文信息
# 处理错误的几种方式
根据调用方的需求,错误处理有不同策略:
| 策略 | 代码示例 | 适用场景 |
|---|---|---|
| 直接返回 | 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))))
}
}
这个例子展示了:
- 分层错误体系:哨兵 → 验证错误 → 数据库错误 → 服务错误
- 错误包装链:每层都保留底层错误信息
- 灵活的错误检查:用
errors.Is检查值,用errors.As提取信息 - 自定义匹配逻辑:
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 思考题
- 解释题:为什么 Go 选择"错误是值"而不是异常?这种设计有什么优缺点?
- 代码题:写一个函数,递归打印错误链的所有层级(用
errors.Unwrap)。 - 设计题:设计一个文件上传的错误体系,要区分"文件太大"、"格式不支持"、"网络超时"等情况。
- 对比题:
errors.Is和errors.As有什么区别?各在什么场景下使用? - 实战题:改造以下代码,使其符合 Go 错误处理最佳实践:
func process() { data, _ := ioutil.ReadFile("config.json") var config map[string]any json.Unmarshal(data, &config) fmt.Println(config["version"]) } - 原理题:画图说明
panic的传播路径,并解释为什么recover必须在defer里。 - 陷阱题:以下代码输出什么?为什么?
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)) } - 工程题:在一个大型项目中,如何统一错误处理风格?请制定 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 让它们通信。