基础错误处理方法
返回错误值(最常用)
func ReadFile(filename string) ([]byte, error) { file, err := os.Open(filename) if err != nil { return nil, err } defer file.Close() data, err := io.ReadAll(file) if err != nil { return nil, err } return data, nil }
func main() { data, err := ReadFile("config.yaml") if err != nil { log.Printf("文件读取失败: %v", err) } }
|
适用场景:
- 大多数常规函数调用
- I/O 操作、网络请求、数据库查询等可能失败的操作
- 需要立即处理错误的场景
错误包装(Error Wrapping)
Go 1.13+ 引入了错误包装机制
func LoadConfig() (Config, error) { data, err := ReadFile("config.yaml") if err != nil { return Config{}, fmt.Errorf("加载配置失败: %w", err) } var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { return Config{}, fmt.Errorf("解析配置失败: %w", err) } return cfg, nil }
func main() { cfg, err := LoadConfig() if err != nil { if errors.Is(err, os.ErrNotExist) { log.Println("配置文件不存在") } log.Printf("配置加载失败: %v", err) } }
|
适用场景:
- 需要添加上下文信息到错误中
- 保留原始错误信息供后续检查
- 构建错误调用链以帮助调试
高级错误处理模式
自定义错误类型
type APIError struct { StatusCode int Message string Err error }
func (e *APIError) Error() string { if e.Err != nil { return fmt.Sprintf("API错误(%d): %s [%v]", e.StatusCode, e.Message, e.Err) } return fmt.Sprintf("API错误(%d): %s", e.StatusCode, e.Message) }
func (e *APIError) Unwrap() error { return e.Err }
func CallAPI(url string) error { resp, err := http.Get(url) if err != nil { return &APIError{ StatusCode: 0, Message: "请求失败", Err: err, } } defer resp.Body.Close() if resp.StatusCode >= 400 { return &APIError{ StatusCode: resp.StatusCode, Message: "API返回错误状态", } } return nil }
func main() { err := CallAPI("https://api.example.com/data") if err != nil { var apiErr *APIError if errors.As(err, &apiErr) { log.Printf("API错误: 状态码 %d, 消息: %s", apiErr.StatusCode, apiErr.Message) if apiErr.StatusCode == 429 { } } else { log.Printf("其他错误: %v", err) } } }
|
适用场景:
- 需要携带额外错误信息(如状态码、错误代码等)
- API 错误处理
- 需要根据错误类型进行不同处理的场景
错误哨兵(Sentinel Errors)
var ( ErrUserNotFound = errors.New("用户不存在") ErrInvalidRequest = errors.New("无效请求") )
func GetUser(id int) (*User, error) { if id <= 0 { return nil, ErrInvalidRequest } user, exists := userDB[id] if !exists { return nil, ErrUserNotFound } return user, nil }
func main() { user, err := GetUser(42) if err != nil { if errors.Is(err, ErrUserNotFound) { } else if errors.Is(err, ErrInvalidRequest) { } } }
|
适用场景:
- 定义可预期的、明确的错误条件
- 公共库中的标准错误定义
- 需要直接比较错误的场景
错误处理中间件
type HandlerFunc func(http.ResponseWriter, *http.Request) error
func ErrorMiddleware(next HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { err := next(w, r) if err != nil { switch e := err.(type) { case *APIError: w.WriteHeader(e.StatusCode) json.NewEncoder(w).Encode(map[string]string{"error": e.Message}) case ValidationError: w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(e.Errors) default: w.WriteHeader(http.StatusInternalServerError) log.Printf("内部错误: %v", err) } } } }
func main() { http.Handle("/user", ErrorMiddleware(userHandler)) http.ListenAndServe(":8080", nil) }
func userHandler(w http.ResponseWriter, r *http.Request) error { userID := r.URL.Query().Get("id") if userID == "" { return &APIError{ StatusCode: http.StatusBadRequest, Message: "缺少用户ID", } } return nil }
|
适用场景:
- Web 服务中的统一错误处理
- 需要将错误转换为 HTTP 响应的场景
- 集中错误日志记录
错误处理辅助工具
错误检查辅助函数
func Check(err error) { if err != nil { log.Fatalf("致命错误: %v", err) } }
func CheckWithContext(err error, context string) { if err != nil { log.Fatalf("%s: %v", context, err) } }
func main() { file, err := os.Open("data.txt") CheckWithContext(err, "打开文件失败") defer file.Close() }
|
适用场景:
- 简单脚本或快速原型
- 错误需要立即终止程序的情况
- 避免重复的错误检查代码
延迟错误处理(defer)
func ProcessFile(filename string) (err error) { file, err := os.Open(filename) if err != nil { return err } defer func() { closeErr := file.Close() if closeErr != nil && err == nil { err = fmt.Errorf("文件关闭失败: %w", closeErr) } }() return nil }
|
适用场景:
- 资源清理操作(关闭文件、数据库连接等)
- 需要将多个错误合并处理的场景
- 确保资源正确释放的情况下处理可能的错误
错误重试机制
func Retry(attempts int, delay time.Duration, fn func() error) error { var err error for i := 0; i < attempts; i++ { err = fn() if err == nil { return nil } if i < attempts-1 { time.Sleep(delay) delay *= 2 } } return fmt.Errorf("操作失败(尝试 %d 次): %w", attempts, err) }
func main() { err := Retry(3, 1*time.Second, func() error { return CallUnstableAPI() }) if err != nil { log.Printf("最终失败: %v", err) } }
|
适用场景:
- 网络请求等可能临时失败的操作
- 数据库连接等可能暂时不可用的资源
- 需要指数退避策略的重试场景
错误处理最佳实践
错误处理原则
- 尽早处理:在错误发生的地方或最近的地方处理
- 明确上下文:为错误添加足够的信息
- 避免忽略错误:不要使用
_
忽略错误
- 错误应可追溯:使用错误包装保持原始错误信息
- 区分错误类型:使用自定义错误或哨兵错误区分不同错误情况
错误日志记录
func LogError(err error, context ...string) { if err == nil { return } msg := "错误发生" if len(context) > 0 { msg = context[0] } log.WithFields(log.Fields{ "error": err.Error(), "context": context, "stack": getStackTrace(), }).Error(msg) }
func main() { if err := CriticalOperation(); err != nil { LogError(err, "执行关键操作失败") } }
|
错误处理策略矩阵
错误类型 |
处理策略 |
示例 |
可恢复错误 |
重试、回退、降级 |
网络请求失败、文件锁定 |
输入错误 |
验证并返回给用户 |
表单验证失败、无效参数 |
配置错误 |
启动时检查并终止 |
缺少必要配置、无效配置 |
代码错误 |
记录日志并修复 |
空指针解引用、逻辑错误 |
外部依赖错误 |
隔离并降级 |
第三方服务不可用 |
资源不足 |
扩容或限制 |
内存不足、磁盘空间不足 |
特定场景的错误处理
并发错误处理
func ProcessConcurrently(jobs []Job) error { var wg sync.WaitGroup errCh := make(chan error, len(jobs)) for _, job := range jobs { wg.Add(1) go func(j Job) { defer wg.Done() if err := j.Process(); err != nil { errCh <- fmt.Errorf("处理作业 %s 失败: %w", j.ID, err) } }(job) } go func() { wg.Wait() close(errCh) }() var errs []error for err := range errCh { errs = append(errs, err) } if len(errs) > 0 { return fmt.Errorf("%d 个作业失败: %w", len(errs), errors.Join(errs...)) } return nil }
|
数据库事务错误处理
func TransferMoney(ctx context.Context, db *sql.DB, from, to string, amount int) error { tx, err := db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("开始事务失败: %w", err) } defer func() { if err != nil { tx.Rollback() } }() if _, err := tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from); err != nil { return fmt.Errorf("扣款失败: %w", err) } if _, err := tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to); err != nil { return fmt.Errorf("存款失败: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("提交事务失败: %w", err) } return nil }
|
HTTP 客户端错误处理
func GetWithRetry(ctx context.Context, url string, retries int) ([]byte, error) { var body []byte err := Retry(retries, 1*time.Second, func() error { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 500 { return fmt.Errorf("服务器错误: %s", resp.Status) } if resp.StatusCode >= 400 { return fmt.Errorf("客户端错误: %s", resp.Status) } body, err = io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("读取响应失败: %w", err) } return nil }) return body, err }
|
错误处理工具和库
标准库工具
if errors.Is(err, sql.ErrNoRows) { }
var validationErr ValidationError if errors.As(err, &validationErr) { }
originalErr := errors.Unwrap(err)
combinedErr := errors.Join(err1, err2, err3)
|
第三方库推荐
pkg/errors:提供强大的错误包装和堆栈跟踪
import "github.com/pkg/errors"
func LoadConfig() error { _, err := os.Open("config.yaml") if err != nil { return errors.Wrap(err, "打开配置文件失败") } }
|
go.uber.org/multierr:处理多个错误
import "go.uber.org/multierr"
func CloseAll(closers []io.Closer) error { var err error for _, closer := range closers { err = multierr.Append(err, closer.Close()) } return err }
|
sentry-go:错误监控和报警
import "github.com/getsentry/sentry-go"
func init() { sentry.Init(sentry.ClientOptions{ Dsn: "your-sentry-dsn", }) }
func main() { if err := CriticalOperation(); err != nil { sentry.CaptureException(err) log.Fatal(err) } }
|
错误处理反模式
错误忽略
file, _ := os.Open("important.txt") file.Write(data)
|
过度泛化的错误处理
result, err := ProcessData() if err != nil { log.Fatal("发生错误") }
|
错误信息暴露
err := db.QueryRow("SELECT * FROM users").Scan(&user) if err != nil { http.Error(w, "数据库错误: "+err.Error(), 500) }
|
错误处理位置不当
func A() error { return B() }
func B() error { return C() }
func C() error { return errors.New("具体错误") }
|
errors.Is和errors.As
在 Go 语言中,errors.Is
和 errors.As
是用于处理错误链的核心函数(自 Go 1.13 引入)。它们能递归检查被包装(wrapped)的错误,适用于现代 Go 错误处理模式。以下是详细对比和使用场景:
errors.Is
作用:检查错误链中是否存在特定值的错误(常与哨兵错误配合)
签名:
func Is(err, target error) bool
|
使用场景:
检查预定义的哨兵错误(Sentinel Errors) 如 io.EOF
、os.ErrNotExist
等。
if errors.Is(err, os.ErrNotExist) { }
|
检查自定义哨兵错误
var ErrInvalidInput = errors.New("invalid input")
if errors.Is(err, ErrInvalidInput) { }
|
递归检查被包装的错误 即使错误被多层 fmt.Errorf("... %w ...", err)
包装,也能穿透检查。
err := fmt.Errorf("layer2: %w", fmt.Errorf("layer1: %w", os.ErrPermission)) errors.Is(err, os.ErrPermission)
|
errors.As
作用:从错误链中提取特定类型的错误(常与自定义错误类型配合)。
签名:
func As(err error, target any) bool
|
使用场景:提取自定义错误类型的详细信息当错误类型包含额外字段(如状态码、上下文)时。
type MyError struct { Code int Message string } func (e *MyError) Error() string { return e.Message }
var me *MyError if errors.As(err, &me) { fmt.Println("错误代码:", me.Code) }
|
处理实现了某接口的错误 ,例如提取 net.Error
类型错误:
var netErr net.Error if errors.As(err, &netErr) && netErr.Timeout() { }
|
穿透多层错误包装提取类型 类似 errors.Is
,但目标是类型而非值。
err := fmt.Errorf("wrapper: %w", &MyError{Code: 404}) var customErr *MyError errors.As(err, &customErr)
|
关键区别总结
特性 |
errors.Is |
errors.As |
目标 |
错误值(哨兵错误) |
错误类型(自定义错误结构体/接口) |
返回值 |
bool (是否匹配) |
bool (是否成功提取) |
典型用途 |
检查特定错误是否出现 |
提取错误中的附加数据 |
参数 target |
error 类型(如 os.ErrNotExist ) |
指针(如 &MyError{} 或 &netErr ) |
使用注意事项
errors.As
的 target
必须是指针:
var e *MyError errors.As(err, &e)
errors.As(err, e)
|
优先使用 errors.Is/As
而非 ==
或类型断言 。传统方式无法处理被包装的错误:
if err == os.ErrNotExist { ... } if e, ok := err.(*MyError); ok { ... }
|
自定义错误需实现 Unwrap() error
方法 。若自定义错误需要被包装,需实现该方法以便 errors.Is/As
能递归检查:
type MyError struct{ Msg string } func (e *MyError) Error() string { return e.Msg } func (e *MyError) Unwrap() error { return e.InnerError }
|
实践示例
var ErrAuthFailed = errors.New("auth failed")
type DatabaseError struct { Query string Err error } func (e *DatabaseError) Error() string { return fmt.Sprintf("query %s failed: %v", e.Query, e.Err) } func (e *DatabaseError) Unwrap() error { return e.Err }
err := fmt.Errorf("service failed: %w", &DatabaseError{ Query: "SELECT * FROM users", Err: ErrAuthFailed, })
if errors.Is(err, ErrAuthFailed) { fmt.Println("认证失败") }
var dbErr *DatabaseError if errors.As(err, &dbErr) { fmt.Println("失败查询:", dbErr.Query) }
|
另一个示例
package main
import ( "errors" "fmt" )
type MyError struct { Code int Message string Err error }
func (e *MyError) Error() string { if e.Err != nil { return fmt.Sprintf("%s: %v", e.Message, e.Err) } return e.Message }
func (e *MyError) Unwrap() error { return e.Err }
var ErrNotFound = errors.New("not found")
func main() { err := &MyError{ Code: 404, Message: "resource not available", Err: ErrNotFound, }
if errors.Is(err, ErrNotFound) { fmt.Println("错误匹配: Not Found") }
var myErr *MyError if errors.As(err, &myErr) { fmt.Printf("错误类型匹配: Code=%d, Message=%s\n", myErr.Code, myErr.Message) } }
|
运行结果如下:
错误匹配: Not Found 错误类型匹配: Code=404, Message=resource not available
|
若不实现Unwrap
package main
import ( "errors" "fmt" )
type MyError struct { Code int Message string Err error }
func (e *MyError) Error() string { if e.Err != nil { return fmt.Sprintf("%s: %v", e.Message, e.Err) } return e.Message }
var ErrNotFound = errors.New("not found")
func main() { err := &MyError{ Code: 404, Message: "resource not available", Err: ErrNotFound, }
if errors.Is(err, ErrNotFound) { fmt.Println("错误匹配: Not Found") }
var myErr *MyError if errors.As(err, &myErr) { fmt.Printf("错误类型匹配: Code=%d, Message=%s\n", myErr.Code, myErr.Message) } }
|
则运行结果如下:
错误类型匹配: Code=404, Message=resource not available
|
选择原则
- 需要检查特定错误值(如
io.EOF
) → errors.Is
- 需要访问错误中的结构化数据(如错误码、上下文) →
errors.As
- 若错误是简单字符串,无需额外数据 → 直接使用
err.Error()
或 fmt
打印