Skip to content
清晨的一缕阳光
返回

Go 错误处理最佳实践

Go 错误处理最佳实践

错误处理是 Go 语言最显著的特征之一。与传统的异常机制不同,Go 采用显式的错误返回值模式。本文将深入探讨 Go 的错误处理机制,分享最佳实践和常见陷阱。

一、错误处理基础

1.1 错误返回值模式

Go 使用多返回值机制返回错误:

// 标准模式
file, err := os.Open("config.json")
if err != nil {
    // 处理错误
    log.Printf("打开文件失败:%v", err)
    return err
}
defer file.Close()

// 处理成功逻辑
data, err := io.ReadAll(file)
if err != nil {
    return fmt.Errorf("读取文件失败:%w", err)
}

1.2 error 接口

Go 的 error 是一个内置接口:

type error interface {
    Error() string
}

// 简单错误
err := errors.New("something went wrong")
fmt.Println(err.Error())  // something went wrong

// 格式化错误
err := fmt.Errorf("用户 %s 不存在", "alice")
fmt.Println(err.Error())  // 用户 alice 不存在

1.3 错误处理原则

// ✅ 正确处理错误
result, err := doSomething()
if err != nil {
    return err  // 或记录日志、重试等
}
use(result)

// ❌ 忽略错误(除非明确不需要)
result, _ := doSomething()  // 危险!
use(result)

// ✅ 明确忽略(使用空白标识符)
_, err = file.WriteString("data")
if err != nil {
    log.Printf("写入失败:%v", err)
}
// 或者
_ = file.Close()  // 明确忽略

二、错误包装(Go 1.13+)

2.1 为什么要包装错误

错误包装可以:

  1. 添加上下文信息
  2. 保留原始错误
  3. 便于错误追踪
// 不包装:丢失上下文
func loadConfig() error {
    data, err := os.ReadFile("config.json")
    if err != nil {
        return err  // 调用者不知道是在加载配置时出错
    }
    // ...
    return nil
}

// 包装:添加上下文
func loadConfig() error {
    data, err := os.ReadFile("config.json")
    if err != nil {
        return fmt.Errorf("加载配置失败:%w", err)
    }
    // ...
    return nil
}

2.2 错误包装语法

// 使用 %w 包装错误(只能包装一个)
err := fmt.Errorf("连接数据库失败:%w", originalErr)

// 使用 %v 或 %s 不包装(仅显示)
err := fmt.Errorf("操作失败:%v", originalErr)  // 不保留原始错误

// 多层包装
err := fmt.Errorf("第 1 层:%w", 
         fmt.Errorf("第 2 层:%w", 
         fmt.Errorf("第 3 层:%v", baseErr)))

2.3 错误解包

Go 1.13 提供了 errors.Iserrors.As

// 检查错误类型
if errors.Is(err, os.ErrNotExist) {
    log.Println("文件不存在,使用默认配置")
}

// 提取自定义错误类型
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    log.Printf("路径错误:%s", pathErr.Path)
}

// 等价于旧写法(不推荐)
if err == os.ErrNotExist {
    // ...
}

2.4 自定义可解包错误

type AppError struct {
    Code    string
    Message string
    Err     error  // 内部错误
}

// 实现 Error 方法
func (e *AppError) Error() string {
    return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}

// 实现 Unwrap 方法(Go 1.13+)
func (e *AppError) Unwrap() error {
    return e.Err
}

// 使用
func getUser(id string) (*User, error) {
    user, err := db.FindUser(id)
    if err != nil {
        return nil, &AppError{
            Code:    "USER_NOT_FOUND",
            Message: fmt.Sprintf("用户 %s 不存在", id),
            Err:     err,
        }
    }
    return user, nil
}

// 检查
_, err := getUser("123")
var appErr *AppError
if errors.As(err, &appErr) {
    if appErr.Code == "USER_NOT_FOUND" {
        // 处理用户不存在
    }
}

三、自定义错误类型

3.1 简单错误类型

// 定义错误类型
type ValidationError struct {
    Field   string
    Message string
}

// 实现 error 接口
func (e *ValidationError) Error() string {
    return fmt.Sprintf("验证失败 [%s]: %s", e.Field, e.Message)
}

// 使用
func validateEmail(email string) error {
    if !strings.Contains(email, "@") {
        return &ValidationError{
            Field:   "email",
            Message: "必须包含 @ 符号",
        }
    }
    return nil
}

// 处理
if err := validateEmail("invalid"); err != nil {
    var vErr *ValidationError
    if errors.As(err, &vErr) {
        log.Printf("字段 %s 验证失败:%s", vErr.Field, vErr.Message)
    }
}

3.2 错误哨兵(Sentinel Errors)

预定义的错误值,用于精确匹配:

// 定义包级别的错误
var (
    ErrNotFound      = errors.New("not found")
    ErrUnauthorized  = errors.New("unauthorized")
    ErrInvalidInput  = errors.New("invalid input")
)

// 使用
func getUser(id string) (*User, error) {
    if id == "" {
        return nil, ErrInvalidInput
    }
    // ...
    if !exists {
        return nil, ErrNotFound
    }
    return user, nil
}

// 检查
user, err := getUser("")
if errors.Is(err, ErrNotFound) {
    // 处理未找到
} else if errors.Is(err, ErrInvalidInput) {
    // 处理无效输入
}

3.3 错误工厂函数

// 错误工厂
func NewNotFoundError(resource, id string) error {
    return &NotFoundError{
        Resource: resource,
        ID:       id,
    }
}

type NotFoundError struct {
    Resource string
    ID       string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s %s not found", e.Resource, e.ID)
}

// 使用
err := NewNotFoundError("user", "123")
// 输出:user 123 not found

四、错误处理模式

4.1 错误转换

在不同层之间转换错误类型:

// DAO 层
func (d *UserDAO) FindByID(id string) (*User, error) {
    row := d.db.QueryRow("SELECT * FROM users WHERE id = ?", id)
    if row.Err() != nil {
        if errors.Is(row.Err(), sql.ErrNoRows) {
            return nil, domain.ErrUserNotFound
        }
        return nil, fmt.Errorf("数据库查询失败:%w", row.Err())
    }
    // ...
}

// Service 层
func (s *UserService) GetUser(id string) (*UserDTO, error) {
    user, err := s.userDAO.FindByID(id)
    if err != nil {
        if errors.Is(err, domain.ErrUserNotFound) {
            return nil, apperror.NewNotFound("用户", id)
        }
        return nil, apperror.NewInternal("获取用户失败", err)
    }
    // ...
}

// Handler 层
func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")
    user, err := h.userService.GetUser(id)
    if err != nil {
        var appErr *apperror.AppError
        if errors.As(err, &appErr) {
            switch appErr.Type {
            case apperror.TypeNotFound:
                http.Error(w, appErr.Message, http.StatusNotFound)
            case apperror.TypeInternal:
                http.Error(w, "服务器错误", http.StatusInternalServerError)
            }
        }
        return
    }
    json.NewEncoder(w).Encode(user)
}

4.2 错误聚合

收集多个错误:

type MultiError []error

func (m MultiError) Error() string {
    if len(m) == 0 {
        return ""
    }
    msgs := make([]string, len(m))
    for i, err := range m {
        msgs[i] = err.Error()
    }
    return strings.Join(msgs, "; ")
}

// 使用
func validateUser(user *User) error {
    var errs MultiError

    if user.Name == "" {
        errs = append(errs, errors.New("姓名不能为空"))
    }
    if user.Age < 0 {
        errs = append(errs, errors.New("年龄不能为负"))
    }
    if user.Email == "" {
        errs = append(errs, errors.New("邮箱不能为空"))
    }

    if len(errs) > 0 {
        return errs
    }
    return nil
}

4.3 重试错误处理

type RetryableError struct {
    Err error
}

func (e *RetryableError) Error() string {
    return fmt.Sprintf("可重试错误:%v", e.Err)
}

func (e *RetryableError) Unwrap() error {
    return e.Err
}

// 重试函数
func doWithRetry(fn func() error, maxRetries int) error {
    var err error
    for i := 0; i < maxRetries; i++ {
        err = fn()
        if err == nil {
            return nil
        }

        // 检查是否可重试
        var retryErr *RetryableError
        if !errors.As(err, &retryErr) {
            return err  // 不可重试,直接返回
        }

        log.Printf("重试 %d/%d: %v", i+1, maxRetries, err)
        time.Sleep(time.Second * time.Duration(i+1))
    }
    return err
}

// 使用
err := doWithRetry(func() error {
    resp, err := http.Get("https://api.example.com/data")
    if err != nil {
        return &RetryableError{Err: err}
    }
    defer resp.Body.Close()
    
    if resp.StatusCode >= 500 {
        return &RetryableError{
            Err: fmt.Errorf("服务器错误:%d", resp.StatusCode),
        }
    }
    return nil
}, 3)

4.4 延迟错误处理

// 使用 defer 统一处理错误
func processFile(filename string) (err error) {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer func() {
        if closeErr := file.Close(); closeErr != nil {
            err = fmt.Errorf("关闭文件失败:%w", closeErr)
        }
    }()

    // 处理逻辑
    data, err := io.ReadAll(file)
    if err != nil {
        return fmt.Errorf("读取文件失败:%w", err)
    }

    return process(data)
}

五、panic 与 recover

5.1 何时使用 panic

使用场景

  1. 不可恢复的错误
func init() {
    if config == nil {
        panic("配置未初始化")
    }
}
  1. 编程错误
func MustParse(s string) *Config {
    cfg, err := Parse(s)
    if err != nil {
        panic(err)  // 调用者的错误
    }
    return cfg
}
  1. 开发阶段检查
func process(items []Item) {
    if len(items) == 0 {
        panic("items 不能为空")
    }
    // ...
}

5.2 recover 使用

// 恢复 panic
func safeExecute(fn func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("执行失败:%v", r)
        }
    }()

    fn()
    return nil
}

// HTTP 中间件
func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if r := recover(); r != nil {
                log.Printf("Panic recovered: %v", r)
                http.Error(w, "Internal Server Error", 500)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

5.3 不要滥用 recover

// ❌ 不推荐:掩盖所有错误
func doSomething() {
    defer func() {
        recover()  // 吞掉所有 panic
    }()
    // 危险代码
}

// ✅ 推荐:记录并重新抛出
func doSomething() (err error) {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("Panic: %v", r)
            err = fmt.Errorf("内部错误:%v", r)
        }
    }()
    // 正常逻辑
}

六、错误日志记录

6.1 结构化日志

type Logger interface {
    Error(msg string, keysAndValues ...interface{})
}

func processOrder(orderID string) error {
    order, err := db.GetOrder(orderID)
    if err != nil {
        log.Error("获取订单失败",
            "order_id", orderID,
            "error", err,
        )
        return fmt.Errorf("获取订单失败:%w", err)
    }
    // ...
}

6.2 错误堆栈追踪

// 使用第三方库记录堆栈
import "github.com/pkg/errors"

func process() error {
    _, err := os.Open("notexist.txt")
    if err != nil {
        return errors.WithStack(err)  // 添加堆栈
    }
    // ...
}

// 输出完整堆栈
func main() {
    if err := process(); err != nil {
        log.Printf("%+v", err)  // %+v 打印堆栈
    }
}

6.3 错误分级

type ErrorLevel int

const (
    LevelDebug ErrorLevel = iota
    LevelInfo
    LevelWarn
    LevelError
    LevelFatal
)

type AppError struct {
    Level   ErrorLevel
    Code    string
    Message string
    Err     error
}

func (e *AppError) Error() string {
    return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
}

// 使用
func handler(w http.ResponseWriter, r *http.Request) {
    err := doSomething()
    if err != nil {
        var appErr *AppError
        if errors.As(err, &appErr) {
            switch appErr.Level {
            case LevelDebug:
                log.Debug(appErr.Message)
            case LevelError:
                log.Error(appErr.Message, "error", appErr.Err)
                http.Error(w, "服务器错误", 500)
            case LevelFatal:
                log.Fatal(appErr.Message, "error", appErr.Err)
            }
        }
    }
}

七、最佳实践总结

7.1 DO(推荐做法)

// ✅ 始终检查错误
result, err := doSomething()
if err != nil {
    return fmt.Errorf("操作失败:%w", err)
}

// ✅ 添加上下文信息
if err != nil {
    return fmt.Errorf("处理用户 %s 失败:%w", userID, err)
}

// ✅ 使用 errors.Is 和 errors.As
if errors.Is(err, os.ErrNotExist) {
    // 处理
}

// ✅ 定义哨兵错误
var ErrNotFound = errors.New("not found")

// ✅ 使用有意义的错误消息
return errors.New("用户名不能为空")

7.2 DON’T(避免做法)

// ❌ 忽略错误
result, _ := doSomething()

// ❌ 空错误检查
if err != nil {
    // 什么都不做
}

// ❌ 模糊的错误消息
return errors.New("error")

// ❌ 直接比较错误(使用 errors.Is)
if err == os.ErrNotExist {  // 不推荐
}

// ❌ 过度使用 panic
if err != nil {
    panic(err)  // 除非是真正不可恢复的错误
}

7.3 错误处理检查清单

八、常用错误处理库

8.1 标准库

import (
    "errors"      // 基础错误
    "fmt"         // 错误包装
)

8.2 第三方库

// github.com/pkg/errors - 堆栈追踪
import "github.com/pkg/errors"
err := errors.Wrap(originalErr, "context")

// github.com/go-playground/validator - 验证错误
import "github.com/go-playground/validator/v10"
validate := validator.New()
err := validate.Struct(data)

// go.uber.org/multierr - 错误合并
import "go.uber.org/multierr"
multierr.Append(&err, err1)
multierr.Append(&err, err2)

总结

Go 的错误处理哲学可以概括为:显式、简洁、可追踪

特性说明
显式处理错误是值,需要显式检查
错误包装使用 %w 添加上下文
类型安全使用 errors.Is/As 检查类型
适度 panic仅用于不可恢复的错误

掌握这些最佳实践,能帮助你写出更健壮、更易维护的 Go 代码。

参考资料


分享这篇文章到:

上一篇文章
Go GMP 调度模型详解
下一篇文章
Go 指针与内存安全