Go 的错误处理哲学:error 是值,不是异常
一、Go 的错误处理哲学
1.1 与异常机制的区别
| 特性 | Go(error) | Java/Python(异常) |
|---|---|---|
| 表现形式 | 普通返回值 | try-catch 机制 |
| 调用开销 | 零(就是多返回一个值) | 较高(栈展开) |
| 强制处理 | 编译器检查(必须处理) | 可以不 catch |
| 可预测性 | 高——每个可能出错的地方都明确 | 低——调用链可能随时抛异常 |
1.2 核心原则
// Go 的错误处理模式:每个可能出错的调用都检查返回值
result, err := doSomething()
if err != nil {
// 处理错误:记录日志、返回错误、终止程序等
return fmt.Errorf("doSomething failed: %w", err)
}
// 继续正常逻辑
Go 的哲学:显式胜于隐式。 没有隐藏的控制流,错误路径一目了然。
二、error 接口
2.1 error 的定义
// 标准库中 error 的定义(非常简洁)
type error interface {
Error() string
}
任何实现了 Error() string 方法的类型都是一个 error:
// 自定义错误类型
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on field '%s': %s", e.Field, e.Message)
}
// 使用
err := &ValidationError{Field: "email", Message: "invalid format"}
fmt.Println(err) // validation failed on field 'email': invalid format
2.2 创建错误的几种方式
import (
"errors"
"fmt"
)
// 1. errors.New:简单的静态错误消息
var ErrNotFound = errors.New("not found")
// 使用时
if err == ErrNotFound {
// 处理特定错误
}
// 2. fmt.Errorf:格式化错误消息(最常用)
err := fmt.Errorf("user %d not found", userID)
// 3. %w 动词:包装(wrap)另一个错误
func getUser(id int) (*User, error) {
err := db.Query("...")
if err != nil {
return nil, fmt.Errorf("getUser(%d): %w", id, err)
// %w 保留了原始错误,可以用 errors.Is/As 检查
}
return &user, nil
}
// 4. 自定义错误类型(携带结构化信息)
// 见上方 ValidationError 示例
三、错误处理模式
3.1 基本模式
// 模式1:检查并返回
func processFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("reading %s: %w", path, err)
}
// 处理 data...
return nil // 成功时返回 nil
}
// 模式2:检查并记录日志 + 继续
data, err := fetchConfig()
if err != nil {
log.Printf("config fetch failed, using defaults: %v", err)
data = defaultConfig()
}
// 模式3:检查并终止
file, err := os.Open("critical.txt")
if err != nil {
log.Fatal("无法打开关键文件:", err)
}
defer file.Close()
3.2 错误链(Error Wrapping)
Go 1.13+ 支持错误包装,可以构建错误链:
// 定义 Sentinel 错误
var (
ErrNotFound = errors.New("record not found")
ErrPermission = errors.New("permission denied")
ErrTimeout = errors.New("request timeout")
)
// 包装错误
func findUser(id int) (*User, error) {
result, err := db.Query("SELECT ...", id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("user %d: %w", id, ErrNotFound)
}
return nil, fmt.Errorf("findUser(%d): %w", id, err)
}
return result, nil
}
// 调用方检查错误链
user, err := findUser(42)
if errors.Is(err, ErrNotFound) {
// 处理"未找到"的特定情况
fmt.Println("用户不存在")
} else if err != nil {
// 其他错误
fmt.Println("查询出错:", err)
}
3.3 errors.Is 与 errors.As
// errors.Is:沿错误链查找特定错误值(类似 === 比较)
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件不存在")
}
// errors.As:沿错误链查找特定类型,提取为该类型
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("路径错误:", pathErr.Path)
fmt.Println("底层错误:", pathErr.Err)
}
// errors.Unwrap:获取被包装的原始错误
wrappedErr := fmt.Errorf("context: %w", originalErr)
unwrapped := errors.Unwrap(err) // originalErr
3.4 错误处理的"黄金范式"
// Go 中最典型、最频繁出现的代码模式:
result, err := someOperation()
if err != nil {
return fmt.Errorf("someOperation in context: %w", err)
}
编写规范:
- 错误消息小写开头,不要加标点:
fmt.Errorf("read config: %w", err) - 包含上下文:哪个函数出错、什么参数、什么条件
- 用
%w包装:保留原始错误,方便调用方判断 - 不要只用
%v:%v丢失了错误链信息 - Sentinel 错误用
Err前缀命名:ErrNotFound、ErrTimeout
四、panic 与 recover
4.1 panic — 不可恢复的致命错误
// panic 用于真正的"不应该发生"的情况
func divide(a, b int) int {
if b == 0 {
panic("division by zero") // 不应该传入 0
}
return a / b
}
// 常见的 panic 场景
// - 数组/切片越界
// - 空指针解引用
// - map 写入 nil
// - 类型断言失败(不带 ok 检查时)
4.2 recover — 捕获 panic
// recover 只在 defer 中有效
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
return a / b, nil
}
// 使用
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println(err) // recovered from panic: division by zero
}
4.3 HTTP 服务器中的 recover
// 在 HTTP handler 中 recover,防止单个请求导致服务器崩溃
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.Printf("panic recovered: %v\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}
4.4 panic 使用原则
| 场景 | 使用 panic | 使用 error |
|---|---|---|
| 配置错误(程序启动时) | ✅ panic("invalid config") | |
| 不可恢复的内部错误 | ✅ | |
| 运行时可恢复的错误 | ✅ | |
| 用户输入/外部数据 | ✅ | |
| 库函数对外接口 | ✅ 永远用 error |
原则:库代码绝不 panic,panic 留给应用层的"最后防线"。
五、错误处理最佳实践
5.1 不要忽略错误
// ❌ 不要这样做
data, _ := os.ReadFile("config.txt") // 忽略错误
// ✅ 即使不处理,至少记录
data, err := os.ReadFile("config.txt")
if err != nil {
log.Printf("warning: cannot read config: %v", err)
// 使用默认值或继续
}
5.2 错误返回 nil 表示成功
// Go 的惯例:成功时 error 为 nil
func Find(id int) (*Item, error) {
// 找到:返回 (item, nil)
// 未找到:返回 (nil, ErrNotFound)
// 出错:返回 (nil, fmt.Errorf("...: %w", err))
}
5.3 早期返回(Early Return)
// 使用 if err != nil → return 的"卫语句"风格
// 深度嵌套的 if-else 是反模式
func process(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read: %w", err)
}
parsed, err := parse(data)
if err != nil {
return fmt.Errorf("parse: %w", err)
}
result, err := validate(parsed)
if err != nil {
return fmt.Errorf("validate: %w", err)
}
return save(result)
}
六、本节要点速查
| 概念 | 要点 |
|---|---|
error 接口 | 只需实现 Error() string 方法 |
errors.New | 创建简单错误 |
fmt.Errorf + %w | 创建带上下文的包装错误(最常用) |
errors.Is | 检查错误链中是否包含特定错误值 |
errors.As | 从错误链中提取特定错误类型 |
| Sentinel 错误 | 包级 var ErrXxx = errors.New(...) |
panic | 不可恢复的致命错误,库代码不用 |
recover | 在 defer 中捕获 panic |
| 错误命名 | 小写开头、含上下文、用 %w 包装 |
不要忽略 _ | 每个 error 都要处理,或至少记录日志 |
豫公网安备41108202000982号