第 7 章:错误处理与 panic/recover
jerry北京市2026年5月8日Go 21 次阅读 约 10 分钟

掌握 Go 独特的错误处理哲学、自定义错误、错误包装链,以及 panic/recover 机制。
7.1 error 接口
Go 没有异常机制(try/catch),而是通过返回值处理错误:
// error 是一个内置接口
type error interface {
Error() string
}
// 标准用法
f, err := os.Open("file.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
这种 if err != nil 的模式在 Go 中无处不在,是 Go 的核心设计哲学之一:错误是值,应该被显式处理。
7.2 创建错误
import (
"errors"
"fmt"
)
// 方式一:errors.New
err1 := errors.New("something went wrong")
// 方式二:fmt.Errorf(支持格式化)
name := "config.yaml"
err2 := fmt.Errorf("无法读取文件: %s", name)
7.3 自定义错误类型
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("字段 %s 验证失败: %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{
Field: "age",
Message: "年龄必须在 0-150 之间",
}
}
return nil
}
// 使用
if err := validateAge(-1); err != nil {
// 类型断言获取详细信息
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("字段: %s, 原因: %s\n", ve.Field, ve.Message)
}
}
7.4 错误包装(Go 1.13+)
// 包装错误:使用 %w
originalErr := errors.New("数据库连接失败")
wrappedErr := fmt.Errorf("用户查询失败: %w", originalErr)
fmt.Println(wrappedErr)
// 输出:用户查询失败: 数据库连接失败
// 解包:errors.Unwrap
inner := errors.Unwrap(wrappedErr)
fmt.Println(inner) // 数据库连接失败
errors.Is 和 errors.As
var ErrNotFound = errors.New("not found")
func findUser(id int) error {
return fmt.Errorf("查询用户 %d: %w", id, ErrNotFound)
}
err := findUser(42)
// errors.Is:判断错误链中是否包含特定错误
if errors.Is(err, ErrNotFound) {
fmt.Println("用户不存在")
}
// errors.As:从错误链中提取特定类型的错误
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println(ve.Field)
}
errors.Is vs ==:
==只比较当前层errors.Is会遍历整个错误链
7.5 错误处理最佳实践
// 1. 尽早返回,减少嵌套
func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("打开文件失败: %w", err)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("读取文件失败: %w", err)
}
// 处理 data...
return nil
}
// 2. 定义哨兵错误(Sentinel Error)
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInternal = errors.New("internal error")
)
// 3. 添加上下文信息
func getUser(id int) (*User, error) {
user, err := db.Query(id)
if err != nil {
return nil, fmt.Errorf("getUser(id=%d): %w", id, err)
}
return user, nil
}
7.6 panic 和 recover
panic 用于不可恢复的错误,recover 用于捕获 panic:
// panic 会终止当前函数,逐层向上传播
func mustPositive(n int) {
if n <= 0 {
panic("必须是正数")
}
}
// recover 只能在 defer 中使用
func safeDiv(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
return a / b, nil
}
result, err := safeDiv(10, 0)
if err != nil {
fmt.Println(err) // panic recovered: runtime error: integer divide by zero
}
什么时候用 panic
- 程序初始化阶段的致命错误(配置缺失、数据库连接失败等)
- 不应该发生的逻辑错误(程序 bug)
- 标准库中的
Must系列函数
// 标准库示例
template.Must(template.New("").Parse("{{.Name}}"))
regexp.MustCompile(`\d+`)
不应该用 panic 的场景:
- 可预期的错误(文件不存在、网络超时等)
- 用户输入验证
- 业务逻辑错误
7.7 面试要点
-
Go 为什么不用 try/catch?
- 错误是值,应该被显式处理
- 避免异常被忽略或滥用
- 代码流程更清晰
-
errors.Is和errors.As的区别?Is判断错误链中是否包含某个特定错误值As从错误链中提取某个特定类型的错误
-
%w和%v包装错误的区别?%w保留错误链,可以用errors.Is/As解包%v只是字符串拼接,丢失了原始错误
-
panic 和 error 的使用场景?
- error:可预期的错误,正常业务流程
- panic:不可恢复的致命错误,程序 bug
练习
- 实现一个自定义错误类型
HTTPError,包含状态码和消息 - 编写一个函数链,每层添加上下文信息,最后用
errors.Is判断根因 - 使用
recover实现一个安全的函数执行器 - 实现一个
Must函数,将(T, error)转换为T(error 时 panic)
评论
登录 后发表评论
暂无评论