当前位置: 首页 > article >正文

Go错误与日志处理—推荐实践

错误的分类

在 Go 语言中,错误是通过实现 error 接口的类型表示的,但不同场景下的错误可以按性质和用途进行分类。以下是 Go 语言错误的常见分类,以及每类错误的解释和示例:


标准错误类型

标准库中定义了许多常见的错误类型,用于表示各种常见的错误场景。以下是一些 Go 标准库中常见的错误类型和相关包:

errors.Newfmt.Errorf

  • 用于创建自定义的错误。

  • 标准库提供的最基础的错误类型。

示例

import ( 
    "errors" 
    "fmt"
) 

err1 := errors.New("this is an error") 
err2 := fmt.Errorf("formatted error: %d", 42)

IO 相关错误

io

  • 包含基础 I/O 操作的错误类型。

常见错误

  • io.EOF:表示流结束(End Of File)。

  • io.ErrUnexpectedEOF:在读取流时遇到意外的 EOF。

  • io.ErrClosedPipe:操作已关闭的管道。

示例

import "io" 
if err == io.EOF { 
    fmt.Println("Reached end of file") 
}

文件操作相关错误

os

  • 处理文件系统相关的错误。

常见错误

  • os.ErrNotExist:文件或目录不存在。

  • os.ErrExist:文件或目录已经存在。

  • os.ErrPermission:权限不足。

  • os.ErrInvalid:无效操作。

示例: 

import "os" 

if errors.Is(err, os.ErrNotExist) {
    fmt.Println("File does not exist") 
}

网络相关错误

net

  • 网络操作相关的错误。

常见错误

  • net.InvalidAddrError:无效地址错误。

  • net.UnknownNetworkError:未知网络类型错误。

  • net.AddrError:地址解析错误。

  • net.DNSError:域名解析错误。

示例

import "net"

_, err := net.LookupHost("invalid_domain")
if dnsErr, ok := err.(*net.DNSError); ok {
     fmt.Println("DNS error:", dnsErr) 
}

 JSON 相关错误

encoding/json

  • JSON 编码和解码的错误。

常见错误

  • json.InvalidUnmarshalError:解码到无效的目标。

  • json.UnmarshalTypeError:JSON 与目标类型不匹配。

示例

import "encoding/json" 

var data interface{}
err := json.Unmarshal([]byte("invalid json"), &data) 

if syntaxErr, ok := err.(*json.SyntaxError); ok { 
    fmt.Println("JSON Syntax Error at offset:", syntaxErr.Offset) 
}

HTTP 相关错误

net/http

  • HTTP 请求与响应相关的错误。

常见错误

  • http.ErrHandlerTimeout:HTTP 处理程序超时。

  • http.ErrBodyNotAllowed:HTTP 请求体不被允许。

示例

import "net/http" 

if errors.Is(err, http.ErrHandlerTimeout) { 
    fmt.Println("HTTP handler timeout") 
}

时间解析相关错误

time

  • 处理时间解析或格式化错误。

常见错误

  • time.ErrBad:时间字符串格式错误。

示例

import "time"

_, err := time.Parse("2006-01-02", "invalid-date")
if err != nil { 
    fmt.Println("Time parsing error:", err) 
}

数据库相关错误

database/sql

  • 数据库操作相关的错误。

常见错误

  • sql.ErrNoRows:查询未返回结果。

  • sql.ErrTxDone:事务已完成,不能再执行操作。

示例

import "database/sql" 

if errors.Is(err, sql.ErrNoRows) { 
    fmt.Println("No rows found") 
}

压缩解压相关错误

compress/gzip

  • 用于处理 gzip 格式的错误。

常见错误

  • gzip.ErrHeader:gzip 文件头错误。


加密解密相关错误

cryptocrypto/x509

  • 加密或证书解析相关错误。

常见错误

  • x509.IncorrectPasswordError:密码错误。

  • x509.UnknownAuthorityError:未知的证书颁发机构。


按错误来源分类

应用级错误

应用程序逻辑中定义的错误,如输入验证失败、业务规则不满足等。这些错误通常由程序员明确定义。

示例

type ValidationError struct { 
    Field string 
    Msg string 
} 

func (e ValidationError) Error() string { 
    return fmt.Sprintf("validation failed on field %s: %s", e.Field, e.Msg) 
}

系统级错误

系统资源相关的错误,包括文件访问、网络问题等。 示例

func readConfig(filename string) error { 
    _, err := os.ReadFile(filename) 
    if err != nil {
        return fmt.Errorf("failed to read config: %w", err) 
    } 
    return nil 
}

第三方库错误

使用第三方库时返回的错误,需要通过文档或代码了解这些错误的含义,并采取适当措施。 示例

func sendMessageToKafka() error { 
    err := producer.SendMessage(message) 
    if err != nil { 
        return fmt.Errorf("kafka producer error: %w", err) 
    } 
    return nil 
}

按错误处理方式分类

可恢复错误

可以通过重新尝试或特定逻辑处理恢复的错误。 示例

func retryOperation(attempts int) error { 
    for i := 0; i < attempts; i++ {
        err := doSomething() 
        if err == nil { 
            return nil 
        } 
        time.Sleep(1 * time.Second) // 等待后重试 
    } 
    return fmt.Errorf("operation failed after %d attempts", attempts) 
}

不可恢复错误

表示程序的逻辑或系统的严重错误,无法通过重新尝试解决,如非法状态、编程错误等。

示例

func mustDivide(a, b int) int { 
    if b == 0 { 
        panic("division by zero") 
    } 
    return a / b 
}


按错误语义分类

用户输入错误

用户提供的输入不满足预期导致的错误。

示例

func validateInput(input string) error { 
    if input == "" {
        return fmt.Errorf("input cannot be empty") 
    } 
    return nil 
}

数据处理错误

数据格式、解析、转换等问题。

示例

func parseInt(value string) (int, error) { 
    num, err := strconv.Atoi(value) 
    if err != nil { 
        return 0, fmt.Errorf("failed to parse integer: %w", err) 
    } 
    return num, nil 
}

网络/IO 错误

网络连接失败、超时、文件系统操作失败等问题。

示例

func fetchData(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return nil, fmt.Errorf("failed to fetch data: %w", err) } defer resp.Body.Close() return io.ReadAll(resp.Body) }

业务逻辑错误

业务逻辑不满足需求导致的错误。

示例

func checkAccountBalance(balance, withdrawAmount float64) error { 
    if withdrawAmount > balance { 
        return fmt.Errorf("insufficient balance") 
    } 
    return nil 
}

按错误表现分类

明确错误

明确的错误通过 error 接口表示,并具有清晰的语义。

示例

return fmt.Errorf("unable to connect to database: %w", err)

模糊错误

返回的错误缺乏上下文信息,不利于调试。

示例

return errors.New("something went wrong") // 不清楚具体问题是什么

总结

Go 中的错误分类可以帮助开发者更清晰地理解错误的来源和性质,从而制定合理的处理策略。推荐:

  1. 使用明确的错误上下文。

  2. 尽量细化错误类型,尤其是应用级错误。

  3. 使用 errors.Iserrors.As 对错误进行分类处理。

  4. 在必要的场景下记录日志,但不要重复记录错误信息。


错误处理规范

错误检查与优先处理

  • 及时检查错误:不要忽略返回的错误值。

  • 优先处理错误:如果发生错误,尽快中止当前流程或采取修复措施。

好的示例:

func readFile(filename string) ([]byte, error) { 
    data, err := os.ReadFile(filename) 
    if err != nil { 
        return nil, fmt.Errorf("failed to read file %s: %w", filename, err) 
    } 
    return data, nil 
}

坏的示例:

func readFile(filename string) ([]byte, error) { 
    data, _ := os.ReadFile(filename) // 忽略错误,可能导致不可预见的问题 
    return data, nil 
}

使用错误包装提供上下文信息

  • 使用 fmt.Errorf%w 包装错误,保留错误链路。

  • 错误信息应清晰表明发生错误的上下文。

好的示例:

func processFile(filename string) error { 
    file, err := os.Open(filename) 
    if err != nil { 
        return fmt.Errorf("failed to open file %s: %w", filename, err) 
    } 
    defer file.Close() // 文件处理逻辑... return nil 
}

坏的示例:

func processFile(filename string) error { 
    _, err := os.Open(filename) 
    if err != nil { 
        return err // 丢失了错误上下文,难以追踪来源 
    } 
    return nil 
}

自定义错误类型

  • 针对特定业务场景,创建自定义错误类型以提供丰富的上下文。

好的示例:

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) 
} 

func validateInput(input string) error { 
    if input == "" { 
        return ValidationError{"input", "cannot be empty"} 
    } 
    return nil 
}

坏的示例:

func validateInput(input string) error { 
    if input == "" { 
        return fmt.Errorf("invalid input") // 错误信息缺乏上下文 
    } 
    return nil 
}

使用 errors.Iserrors.As 检查错误

  • 使用 errors.Is 检查错误是否是某种特定类型。

  • 使用 errors.As 提取并处理特定的错误类型。

对比

特性errors.Iserrors.As
用途检查错误值是否相等或包装目标错误检查错误是否为特定类型
参数错误和目标错误值错误和目标错误类型的指针
返回值布尔值布尔值,目标指针可能会被赋值
支持链式错误
适用场景判断是否是某个特定错误判断是否属于某个特定类型的错误

好的示例:

func handleError(err error) { 
    if errors.Is(err, os.ErrNotExist) { 
        fmt.Println("文件不存在") 
    } 
    var pathErr *os.PathError 
    
    if errors.As(err, &pathErr) { 
        fmt.Printf("路径错误: %s\n", pathErr.Path) 
    } 
}

避免滥用 panic,使用显式错误返回

  • panic 仅用于不可恢复的错误,普通错误应返回 error

  • 提供有意义的错误信息。

好的示例:

func divide(a, b int) (int, error) { 
    if b == 0 { 
        return 0, fmt.Errorf("division by zero") 
    } 
    return a / b, nil 
}

坏的示例:

func divide(a, b int) int { 
    if b == 0 { 
        panic("division by zero") // 滥用 panic,不建议用于常规错误处理 
    } 
    return a / b 
}

日志与错误分离

  • 错误和日志分层:日志应由调用方处理,库函数仅返回错误。

  • 日志通常在服务层或调用者处理,库函数不应记录日志。

好的示例:

func fetchData(url string) ([]byte, error) { 
    resp, err := http.Get(url) 
    if err != nil { 
        return nil, fmt.Errorf("failed to fetch data from %s: %w", url, err) 
    } 
    defer resp.Body.Close() 
    
    if resp.StatusCode != http.StatusOK { 
        return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) 
    } 
    return io.ReadAll(resp.Body) 
}

坏的示例:

func fetchData(url string) ([]byte, error) { 
    resp, err := http.Get(url) 
    if err != nil { 
        log.Printf("error: %v", err) // 不必要的日志记录 
        return nil, err 
    } 
    defer resp.Body.Close() 
    
    return io.ReadAll(resp.Body) 
}

使用 defer 简化资源清理

  • 使用 defer 保证资源在函数退出时被正确释放。

好的示例:

func processLargeFile(filename string) error { 
    file, err := os.Open(filename) 
    if err != nil { 
        return fmt.Errorf("failed to open file: %w", err) 
    } 
    defer file.Close() // 确保资源释放 

    // 文件处理逻辑... 
    return nil 
}

坏的示例:

func processLargeFile(filename string) error { 
    file, err := os.Open(filename) 
    if err != nil { 
        return err 
    } // 如果忘记关闭文件,会导致资源泄露 
    file.Close() 
    return nil 
}

分层处理错误

  • 在业务逻辑层返回错误,允许调用方决定是否记录日志。

  • 在顶层捕获错误并进行统一处理。

好的实践:

// 库函数 
func queryDatabase(query string) ([]Record, error) { 
    rows, err := db.Query(query) 
    if err != nil { 
        return nil, fmt.Errorf("database query failed: %w", err) 
    } 
    defer rows.Close() // 解析数据... 

    return records, nil 
} 

// 应用层 
func handleRequest(query string) { 
    records, err := queryDatabase(query) 
    if err != nil { 
        log.Printf("query error: %v", err) 
        return 
    } 
    fmt.Println(records) 
}

错误与日志处理的推荐实践

不同层级对错误和日志的处理

数据访问层DAL/Repository)

职责:直接与数据库或其他持久化存储交互。

  • 错误处理

    • 返回具体的、易于处理的错误。例如:SQL 执行失败、数据未找到等。

    • 尽量使用错误包装 (fmt.Errorf),为上层提供上下文。

    • 不要记录日志,交由上层决定是否需要记录。

  • 示例

func GetUserByID(id int) (*User, error) { 
    user := &User{} 
    err := db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user.ID, &user.Name) 
    if errors.Is(err, sql.ErrNoRows) { 
        return nil, fmt.Errorf("user with ID %d not found: %w", id, err) 
    } 
    if err != nil { 
        return nil, fmt.Errorf("failed to fetch user: %w", err) 
    } 
    return user, nil 
}

服务层(Service/Use Case)

职责:实现业务逻辑。

  • 错误处理

    • 捕获底层错误并添加业务语义上下文。

    • 根据需要返回特定业务错误或通用错误。

    • 可对一些关键错误进行日志记录(如影响业务流程的错误)。

  • 日志记录

    • 记录错误可能对调试或审计有价值的信息。

    • 日志应包含业务上下文(如用户 ID、请求参数等)。

  • 示例

func ProcessOrder(orderID int) error { 
    order, err := repo.GetOrderByID(orderID) 
    if err != nil { 
        return fmt.Errorf("failed to process order %d: %w", orderID, err) 
    } 
    if order.Status != "pending" { 
        return fmt.Errorf("order %d is not in a pending state", orderID) 
    } 
    // 业务逻辑... 
    return nil 
}

控制器层(Controller/Handler)

职责:处理用户请求并返回响应。

  • 错误处理

    • 将服务层的错误转换为用户友好的消息(HTTP 状态码或自定义响应)。

    • 不应暴露底层实现细节。

  • 日志记录

    • 在请求入口处记录重要的请求信息。

    • 在错误返回时记录错误上下文和请求相关信息。

  • 示例

func OrderHandler(w http.ResponseWriter, r *http.Request) { 
    orderID, err := strconv.Atoi(r.URL.Query().Get("id")) 
    if err != nil { 
        http.Error(w, "invalid order ID", http.StatusBadRequest) 
        return 
    } 
    err = service.ProcessOrder(orderID) 
    if err != nil { 
        log.Printf("failed to process order: %v", err) 
        http.Error(w, "failed to process order", http.StatusInternalServerError) 
        return 
    } 
    w.WriteHeader(http.StatusOK) 
}

错误与日志的推荐实践

错误返回层级

  1. 底层(如 DAL

    1. 返回详细的上下文错误,方便上层理解问题。

    2. 不记录日志,避免重复记录。

  2. 中间层(如 Service)

    1. 包装底层错误,提供业务相关的上下文。

    2. 根据需要选择是否记录关键日志。

  3. 顶层(如 Controller

    1. 转换错误为用户友好的消息。

    2. 记录完整的请求上下文和错误信息。


日志记录层级

  1. 入口点(Controller/Handler)

    1. 记录请求相关的信息(URL、参数、用户身份等)。

    2. 记录最终的响应状态。

  2. 服务层

    1. 记录对业务有重要影响的错误或状态变化。

  3. 数据层

    1. 尽量不记录日志,避免暴露内部实现细节。


常见反模式与改进

  1. 重复记录日志

    1. 底层记录错误,上层再次记录相同错误,导致日志冗余。

    2. 改进:仅在一个明确的层级记录日志。

  2. 暴露内部错误细节

    1. 直接将数据库错误返回到用户端。

    2. 改进:在顶层捕获并转换为用户友好的消息。

  3. 忽略日志上下文

    1. 日志中缺乏关键信息(如用户 ID、操作参数等)。

    2. 改进:在日志中包含足够的上下文信息。


总结

错误处理应遵循逐层封装的原则,每一层专注于自身职责,避免信息泄漏或日志冗余。日志记录应关注调试和审计价值,并在错误信息中添加业务或操作上下文,从而提高系统的可维护性和可观测性。


http://www.kler.cn/a/412996.html

相关文章:

  • 【Flink-scala】DataStream编程模型之 窗口的划分-时间概念-窗口计算程序
  • Vue.js --- 生命周期
  • 嵌入式linux C++通用makefile模板
  • 如何选择最适合企业的ETL解决方案?
  • CRTP mixins EBO
  • 常用的数据结构
  • STM32F103系列单片机通用和复用I/O(GPIO)
  • 容器和它的隔离机制
  • linux模拟HID USB设备及wireshark USB抓包配置
  • 最小生成树-Prim与Kruskal算法
  • day25|leetCode 491.递增子序列,46.全排列 ,47.全排列 II
  • 算法——四数相加 二(leetcode454)
  • 预处理指令
  • Java线程同步Synchronized
  • Kadb中的ecpg编程
  • 如何开发历史题材游戏。
  • C++练级计划->《单例模式》懒汉和饿汉
  • 使用PHP实现用户权限控制系统
  • c++的虚继承说明、案例、代码
  • 网络药理学之薛定谔Schrödinge Maestro:6、分子对接(Glide、Ligand docking)和可视化
  • 【人工智能】Python常用库-TensorFlow常用方法教程
  • C语言编译和链接讲解
  • 【k8s深入学习之 Scheme】全面理解 Scheme 的注册机制、内外部版本、自动转换函数、默认填充函数、Options等机制
  • RocketMQ: 消息过滤,通信组件,服务发现
  • 探索Python WebSocket新境界:picows库揭秘
  • 哈希表理解与底层模拟实现