错误处理策略
本教程共 80 篇 · 第 54 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
54. 错误处理策略
本节目标:搞懂什么时候用 error、什么时候用 panic,学会错误日志记录,理解错误忽略的代价,掌握 errors.Join 批量处理。
error vs panic
§26 详细讲了 panic 与 recover 的机制,以及「什么时候该 panic」的判断原则。这里从策略角度补充几点:
- 能用 error 处理的就用 error,不要用 panic 代替正常的错误返回
- panic 留给真正不该发生的情况:程序初始化失败、不可能到达的代码分支、编程错误(如传了 nil 给不允许 nil 的参数)
- recover 主要用在边界处:HTTP handler、goroutine 入口、库的顶层调用,防止单点 panic 拖垮整个程序
- 不确定就返回 error──这是最安全的默认选择
Note
log.Fatal内部调用os.Exit(1),不是 panic。效果类似(程序退出),但 panic 会执行 defer,os.Exit不会。
错误处理策略选择
策略一:返回错误
最常用的策略。函数返回 error,调用方决定怎么处理:
func ParseConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("读取配置: %w", err)
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("解析配置: %w", err)
}
return &cfg, nil
}
策略二:重试
对于网络错误等临时性错误,可以重试:
func FetchWithRetry(url string, maxRetries int) ([]byte, error) {
var lastErr error
for i := 0; i < maxRetries; i++ {
resp, err := http.Get(url)
if err == nil {
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
lastErr = err
time.Sleep(time.Second * time.Duration(i+1))
}
return nil, fmt.Errorf("重试 %d 次后仍失败: %w", maxRetries, lastErr)
}
策略三:降级
出错时用备用方案:
func GetUserName(id int) string {
name, err := cache.Get(id)
if err != nil {
// 缓存失败,降级到数据库
name, err = db.GetUserName(id)
if err != nil {
return "未知用户" // 最终降级
}
}
return name
}
策略四:记录日志后继续
某些非关键错误可以记录后继续执行:
if err := sendMetrics(); err != nil {
log.Printf("发送监控数据失败(不影响主流程): %v", err)
}
// 继续执行主逻辑
错误忽略的代价
忽略错误是 Go 编程中最危险的坏习惯之一:
// 坏:完全忽略错误
data, _ := os.ReadFile(path)
fmt.Println(string(data)) // 如果文件不存在,data 是 nil,打印空字符串
// 坏:忽略后用 nil 数据
user, _ := db.GetUser(id)
fmt.Println(user.Name) // 如果 GetUser 返回 nil,这里 panic
Warning
_忽略错误时,后续代码可能基于错误的假设运行。文件读取失败时 data 是 nil,数据库查询失败时 user 是 nil。如果不检查错误就使用这些值,很可能导致 nil 指针 panic。
errors.Join 批量错误处理
§52 介绍了 errors.Join(Go 1.20)的基本用法。它在表单验证等需要收集多个错误的场景特别好用:
package main
import (
"errors"
"fmt"
"strings"
)
type FormData struct {
Name string
Email string
Age int
}
func ValidateForm(fd FormData) error {
var errs []error
if fd.Name == "" {
errs = append(errs, errors.New("名字不能为空"))
}
if len(fd.Name) > 50 {
errs = append(errs, errors.New("名字太长"))
}
if fd.Email == "" {
errs = append(errs, errors.New("邮箱不能为空"))
}
if !strings.Contains(fd.Email, "@") {
errs = append(errs, errors.New("邮箱格式不对"))
}
if fd.Age < 0 || fd.Age > 150 {
errs = append(errs, errors.New("年龄不合理"))
}
return errors.Join(errs...)
}
func main() {
err := ValidateForm(FormData{
Name: "",
Email: "invalid-email",
Age: -5,
})
if err != nil {
fmt.Println(err)
// 名字不能为空
// 邮箱格式不对
// 年龄不合理
}
}
如果没有 errors.Join,你只能在第一个错误时就返回,用户一次只能看到一个错误。有了 errors.Join,可以一次性收集所有错误返回,用户体验更好。
Tip
errors.Join传入 nil 会被自动忽略。如果所有参数都是 nil,返回 nil(表示没有错误)。
错误处理检查清单
写代码时过一遍这个清单:
- 是否检查了所有 error 返回值? 不要用
_忽略 - 是否添加了上下文? 用
fmt.Errorf("...: %w", err)包装 - 是否用了正确的比较方式? 用
errors.Is不用== - 是否需要提取错误详情? 用
errors.As提取自定义类型 - 是否在正确的层级处理? 不要在底层 panic,不要在高层忽略
- 日志是否足够? 关键错误要记录日志
- 是否有降级方案? 非关键路径考虑降级
小结
错误处理策略的核心:
- error 用于预期错误,panic 用于不可恢复的错误
- 常用策略:返回错误、重试、降级、记录日志后继续
- 永远不要忽略 error 返回值
errors.Join(Go 1.20)批量收集错误,提升用户体验- recover 用于边界防护,不是日常错误处理工具
到这里,Go 语言的核心知识就学完了。从基础语法到切片、map、指针、结构体、接口、泛型、错误处理,你已经掌握了写 Go 程序需要的全部基础。后面的章节会进入并发编程和标准库,开始写真正的 Go 程序。