Go

Golang的特性十(error错误处理)----从基础到优雅实践

Royal
2023-08-06 / 0 评论 / 6 阅读 / 正在检测是否收录...
错误处理是 Go 语言的核心设计哲学之一。与传统的 try-catch 异常机制不同,Go 采用显式错误检查,强调开发者主动处理可能的失败路径。这种设计虽然提高了代码的可控性,但也对开发者提出了更高的要求。本文将深入探讨 Go 的错误处理模式,并提供实用代码示例。

一、Go 错误处理基础

  1. 错误是什么?
    在 Go 中,错误是一个实现了 error 接口的值:

    type error interface {
        Error() string
    }

    任何实现了 Error() string 方法的类型都可以作为错误返回。

  2. 创建错误
    基础方式:

    // 使用 errors.New
    import "errors"
    err := errors.New("文件未找到")
    
    // 使用 fmt.Errorf(格式化错误)
    file := "config.yaml"
    err := fmt.Errorf("文件 %s 不存在", file)

    带错误类型的高级方式(自定义错误):

    type NotFoundError struct {
        File string
        Line int
    }
    
    func (e *NotFoundError) Error() string {
        return fmt.Sprintf("%s:%d - 未找到", e.File, e.Line)
    }
    
    // 使用
    err := &NotFoundError{"config.go", 42}

二、错误处理模式

  1. 经典检查:if err != nil

    func ReadFile(path string) ([]byte, error) {
        data, err := os.ReadFile(path)
        if err != nil {
            return nil, err // 向上传递原始错误
        }
        return data, nil
    }
    
    // 调用方处理
    content, err := ReadFile("config.yaml")
    if err != nil {
        log.Printf("读取文件失败: %v", err)
        return
    }
  2. 错误断言(类型判断)

    if nfErr, ok := err.(*NotFoundError); ok {
        // 处理特定错误类型
        log.Printf("文件未找到: %s", nfErr.File)
    } else if os.IsNotExist(err) { // 使用标准库工具函数
        log.Print("系统级文件不存在错误")
    } else {
        // 通用错误处理
    }

三、Go 1.13+ 错误增强

  1. 错误包装(Error Wrapping)
    使用 %w 占位符为错误添加上下文:

    func ReadConfig() error {
        _, err := os.ReadFile("config.yaml")
        if err != nil {
            return fmt.Errorf("读取配置文件失败: %w", err)
        }
        return nil
    }
  2. 错误链解包
    使用 errors.Is 和 errors.As 处理包装后的错误:

    err := ReadConfig()
    
    // 检查错误链中是否包含特定类型
    var pathError *os.PathError
    if errors.As(err, &pathError) {
        fmt.Println("底层 PathError:", pathError.Path)
    }
    
    // 检查错误链中是否包含特定值
    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("文件确实不存在")
    }

四、最佳实践指南

何时返回错误?

  • 当函数无法完成其承诺的操作时
  • 遇到不可恢复的外部依赖问题(如网络中断)
  • 接收到非法参数输入

错误处理原则

要做的:

  • 添加上下文:使用 fmt.Errorf("operation failed: %w", err) 丰富错误信息
  • 处理或返回:每个错误应该被处理或明确返回给上层
  • 定义哨兵错误:对重要错误定义可导出的变量:

    var ErrInvalidInput = errors.New("invalid input")

避免的:

  • 忽略错误:

    data, _ = os.ReadFile("file") // 危险!
  • 过度使用 panic:

    if err != nil {
        panic(err) // 仅在不可恢复时使用
    }
  • 暴露底层细节:

    return fmt.Errorf("数据库错误: %v", err) // 可能泄露敏感信息

五、实战示例:分层错误处理

场景:Web 服务中的错误传递

// 数据层错误
type DatabaseError struct {
    Query string
    Err   error
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("查询 %s 失败: %v", e.Query, e.Err)
}

// 业务逻辑层
func GetUser(id int) (*User, error) {
    user, err := db.QueryUser(id)
    if err != nil {
        return nil, &DatabaseError{
            Query: fmt.Sprintf("SELECT * FROM users WHERE id = %d", id),
            Err:   err,
        }
    }
    return user, nil
}

// 控制器层
func UserHandler(w http.ResponseWriter, r *http.Request) {
    user, err := GetUser(123)
    if err != nil {
        var dbErr *DatabaseError
        if errors.As(err, &dbErr) {
            http.Error(w, "数据库错误", http.StatusInternalServerError)
            log.Printf("数据库错误详情: %+v", dbErr) // 记录完整信息
        } else {
            http.Error(w, err.Error(), http.StatusBadRequest)
        }
        return
    }
    json.NewEncoder(w).Encode(user)
}

六、错误日志记录技巧
结构化日志

import "go.uber.org/zap"

logger, _ := zap.NewProduction()

func HandleRequest() {
    err := processRequest()
    if err != nil {
        logger.Error("请求处理失败",
            zap.Error(err),          // 记录错误对象
            zap.String("path", "/api"), // 添加上下文
            zap.Int("status", 500),
        )
    }
}

堆栈跟踪记录
使用第三方库增强错误信息:

import "github.com/pkg/errors"

func ReadConfig() error {
    _, err := os.ReadFile("config.yaml")
    if err != nil {
        return errors.Wrap(err, "读取配置文件失败")
    }
    return nil
}

// 记录错误时打印堆栈
fmt.Printf("%+v\n", err)

七、panic 与 recover
使用准则:

  • panic:仅用于无法恢复的严重错误(如启动配置缺失)
  • recover:在顶层函数(如 HTTP 中间件)中使用
    示例:Web 服务恢复中间件

    func RecoveryMiddleware(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            defer func() {
                if err := recover(); err != nil {
                    log.Printf("Recovered from panic: %v", err)
                    http.Error(w, "服务器内部错误", http.StatusInternalServerError)
                }
            }()
            next.ServeHTTP(w, r)
        })
    }
0

评论

博主关闭了当前页面的评论