跳转至

3.2 错误处理最佳实践

错误处理是构建健壮应用程序的基石,Go语言的错误处理哲学与其他语言有着显著不同,需要我们深入理解和掌握。

学习目标

  • 深入理解Go语言错误处理的设计理念
  • 掌握自定义错误类型的设计与实现
  • 熟练运用错误包装与展开机制
  • 建立完善的错误处理体系

学习内容

error接口的设计理念

Go语言的错误处理建立在简单的error接口之上,这种设计体现了"错误即值"的哲学思想。

package main

import (
    "errors"
    "fmt"
)

// error接口定义
// type error interface {
//     Error() string
// }

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Printf("Error: %s\n", err.Error())
        return
    }
    fmt.Printf("Result: %.2f\n", result)
}

设计理念解析: - 简洁性error接口只有一个方法Error() string - 错误即值:错误被视为普通的值,可以像其他值一样传递和处理 - 显式处理:强制开发者显式检查错误,避免意外忽略 - 与异常对比:不同于其他语言的异常机制,Go使用返回值表示错误,使错误处理流程更加清晰可控

自定义错误类型

对于复杂的应用程序,基本的错误信息往往不够,我们需要创建自定义错误类型来携带更多上下文信息。

package main

import (
    "fmt"
    "time"
)

// 自定义错误类型
type AppError struct {
    Code      int
    Message   string
    Timestamp time.Time
    Context   map[string]interface{}
}

// 实现error接口
func (e *AppError) Error() string {
    return fmt.Sprintf("Error %d: %s (at %v)", e.Code, e.Message, e.Timestamp)
}

// 构造函数
func NewAppError(code int, message string, context map[string]interface{}) *AppError {
    return &AppError{
        Code:      code,
        Message:   message,
        Timestamp: time.Now(),
        Context:   context,
    }
}

// 错误码常量
const (
    ErrNotFound = iota + 1
    ErrPermissionDenied
    ErrInvalidInput
)

func getUser(id int) (string, error) {
    if id < 0 {
        return "", NewAppError(ErrInvalidInput, "user ID cannot be negative", 
            map[string]interface{}{"id": id})
    }

    if id > 100 {
        return "", NewAppError(ErrNotFound, "user not found", 
            map[string]interface{}{"id": id})
    }

    return fmt.Sprintf("User%d", id), nil
}

func main() {
    _, err := getUser(-1)
    if err != nil {
        if appErr, ok := err.(*AppError); ok {
            fmt.Printf("Application error: %s\n", appErr.Error())
            fmt.Printf("Error code: %d, Context: %v\n", appErr.Code, appErr.Context)
        } else {
            fmt.Printf("Unexpected error: %s\n", err.Error())
        }
    }
}

最佳实践: - 为错误分类定义错误码常量 - 在错误中包含时间戳、上下文信息等元数据 - 提供构造函数简化错误创建 - 使用类型断言来检查和处理特定错误类型

错误包装与展开(Go 1.13+)

Go 1.13引入了错误包装机制,允许我们创建错误链,保留原始错误信息的同时添加更多上下文。

package main

import (
    "errors"
    "fmt"
    "os"
)

// 包装错误示例
func readConfig(filepath string) ([]byte, error) {
    data, err := os.ReadFile(filepath)
    if err != nil {
        return nil, fmt.Errorf("read config failed: %w", err)
    }
    return data, nil
}

func loadConfig() error {
    _, err := readConfig("/nonexistent/config.yaml")
    if err != nil {
        return fmt.Errorf("load config failed: %w", err)
    }
    return nil
}

func main() {
    err := loadConfig()
    if err != nil {
        fmt.Printf("Error: %v\n", err)

        // 使用errors.Is检查特定错误
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("File does not exist")
        }

        // 使用errors.As提取特定错误类型
        var pathError *os.PathError
        if errors.As(err, &pathError) {
            fmt.Printf("Path error: %s at path %s\n", pathError.Op, pathError.Path)
        }

        // 展开错误链
        fmt.Println("Unwrapped error chain:")
        for unwrapped := err; unwrapped != nil; unwrapped = errors.Unwrap(unwrapped) {
            fmt.Printf("- %v\n", unwrapped)
        }
    }
}

关键功能: - %w动词:用于包装错误,保留原始错误 - errors.Is:检查错误链中是否存在特定错误 - errors.As:提取错误链中特定类型的错误 - errors.Unwrap:展开错误链,获取底层错误

panic与recover机制

虽然Go鼓励使用错误返回值,但在真正不可恢复的情况下,可以使用panic和recover机制。

package main

import (
    "fmt"
)

func riskyOperation() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Recovered from panic: %v\n", r)
        }
    }()

    fmt.Println("Performing risky operation...")
    panic("something went terribly wrong")
    fmt.Println("This will not be executed") // 不会执行
}

func safeOperation() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Caught panic in safeOperation: %v\n", r)
        }
    }()

    riskyOperation()
    fmt.Println("Risky operation completed") // 如果riskyOperation发生panic,这里不会执行
}

func main() {
    fmt.Println("Calling safe operation...")
    safeOperation()
    fmt.Println("Program continues normally after recovery")

    // 不应滥用的panic示例
    fmt.Println("\n--- Proper error handling vs panic ---")

    // 正确的方式:返回错误
    result, err := properFunction(0)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
    } else {
        fmt.Printf("Result: %d\n", result)
    }

    // 错误的方式:滥用panic
    func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Printf("Recovered from improper panic: %v\n", r)
            }
        }()
        improperFunction(0) // 这会引发panic
    }()
}

func properFunction(value int) (int, error) {
    if value == 0 {
        return 0, fmt.Errorf("value cannot be zero")
    }
    return 100 / value, nil
}

func improperFunction(value int) int {
    if value == 0 {
        panic("value cannot be zero") // 不应使用panic处理预期错误
    }
    return 100 / value
}

使用准则: - panic用于真正不可恢复的错误,如程序逻辑错误 - recover只能在defer函数中使用 - 不应使用panic处理预期内的错误条件 - 在库函数中应尽量避免使用panic,而是返回错误

错误处理的设计模式

在实际应用中,我们需要采用适当的错误处理模式来构建健壮的应用程序。

package main

import (
    "context"
    "errors"
    "fmt"
    "math/rand"
    "time"
)

// 错误传播策略示例
func serviceA(ctx context.Context) error {
    // 模拟随机错误
    if rand.Intn(2) == 0 {
        return errors.New("service A unavailable")
    }
    return nil
}

func serviceB(ctx context.Context) error {
    // 模拟随机错误
    if rand.Intn(2) == 0 {
        return errors.New("service B timeout")
    }
    return nil
}

// 错误聚合处理
func aggregateServices(ctx context.Context) error {
    errCh := make(chan error, 2)

    go func() {
        errCh <- serviceA(ctx)
    }()

    go func() {
        errCh <- serviceB(ctx)
    }()

    // 收集所有错误
    var errs []error
    for i := 0; i < 2; i++ {
        if err := <-errCh; err != nil {
            errs = append(errs, err)
        }
    }

    if len(errs) > 0 {
        return fmt.Errorf("multiple errors occurred: %v", errs)
    }

    return nil
}

// 重试机制设计
func retryOperation(operation func() error, maxRetries int, delay time.Duration) error {
    var err error
    for i := 0; i < maxRetries; i++ {
        if err = operation(); err == nil {
            return nil
        }

        fmt.Printf("Attempt %d failed: %v. Retrying in %v...\n", i+1, err, delay)
        time.Sleep(delay)
    }
    return fmt.Errorf("after %d attempts, last error: %w", maxRetries, err)
}

// 超时与取消处理
func operationWithTimeout(ctx context.Context, duration time.Duration) error {
    select {
    case <-time.After(duration):
        fmt.Println("Operation completed successfully")
        return nil
    case <-ctx.Done():
        return fmt.Errorf("operation canceled: %w", ctx.Err())
    }
}

func main() {
    rand.Seed(time.Now().UnixNano())

    fmt.Println("=== Error Aggregation Example ===")
    ctx := context.Background()
    if err := aggregateServices(ctx); err != nil {
        fmt.Printf("Aggregated error: %v\n", err)
    }

    fmt.Println("\n=== Retry Mechanism Example ===")
    retryErr := retryOperation(func() error {
        if rand.Intn(4) == 0 { // 25%成功率
            return errors.New("temporary failure")
        }
        return nil
    }, 3, 100*time.Millisecond)

    if retryErr != nil {
        fmt.Printf("Final error after retries: %v\n", retryErr)
    } else {
        fmt.Println("Operation succeeded after retry")
    }

    fmt.Println("\n=== Timeout Handling Example ===")
    timeoutCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
    defer cancel()

    if err := operationWithTimeout(timeoutCtx, 200*time.Millisecond); err != nil {
        fmt.Printf("Timeout error: %v\n", err)
    }
}

设计模式总结: - 错误传播:在调用栈中适当包装和传递错误 - 错误聚合:同时执行多个操作,收集所有错误 - 重试机制:对暂时性错误实施有限次数的重试 - 超时处理:使用context设置操作超时,防止无限阻塞

实战练习

练习1:实现自定义错误类型与错误链

package main

import (
    "errors"
    "fmt"
)

// 定义错误码
const (
    ErrCodeDatabase = iota + 1
    ErrCodeNetwork
    ErrCodeValidation
)

// 自定义错误类型
type CustomError struct {
    Code     int
    Message  string
    Original error
}

func (e *CustomError) Error() string {
    if e.Original != nil {
        return fmt.Sprintf("%s (code: %d): %v", e.Message, e.Code, e.Original)
    }
    return fmt.Sprintf("%s (code: %d)", e.Message, e.Code)
}

func (e *CustomError) Unwrap() error {
    return e.Original
}

// 错误构造函数
func NewCustomError(code int, message string, original error) *CustomError {
    return &CustomError{
        Code:     code,
        Message:  message,
        Original: original,
    }
}

// 模拟数据库操作
func dbQuery() error {
    // 模拟底层数据库错误
    return errors.New("connection timeout")
}

// 业务逻辑层
func getUserData(userID int) error {
    if userID <= 0 {
        return NewCustomError(ErrCodeValidation, "invalid user ID", nil)
    }

    err := dbQuery()
    if err != nil {
        return NewCustomError(ErrCodeDatabase, "failed to query database", err)
    }

    return nil
}

func main() {
    // 测试验证错误
    err := getUserData(-1)
    if err != nil {
        fmt.Printf("Validation error: %v\n", err)
    }

    // 测试数据库错误(包装错误)
    err = getUserData(123)
    if err != nil {
        fmt.Printf("Database error: %v\n", err)

        // 检查错误链中是否存在特定错误
        var customErr *CustomError
        if errors.As(err, &customErr) {
            fmt.Printf("Error code: %d\n", customErr.Code)
        }

        // 展开原始错误
        if original := errors.Unwrap(err); original != nil {
            fmt.Printf("Original error: %v\n", original)
        }
    }
}

练习2:设计一个HTTP客户端的错误处理系统

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "math/rand"
    "net/http"
    "time"
)

// HTTP错误类型
type HTTPError struct {
    StatusCode int
    URL        string
    Method     string
    Message    string
}

func (e *HTTPError) Error() string {
    return fmt.Sprintf("HTTP %d %s %s: %s", e.StatusCode, e.Method, e.URL, e.Message)
}

// 客户端结构
type HTTPClient struct {
    Client      *http.Client
    MaxRetries  int
    RetryDelay  time.Duration
    Logger      *log.Logger
}

// 创建新的HTTP客户端
func NewHTTPClient(maxRetries int, retryDelay time.Duration) *HTTPClient {
    return &HTTPClient{
        Client: &http.Client{
            Timeout: 10 * time.Second,
        },
        MaxRetries: maxRetries,
        RetryDelay: retryDelay,
        Logger:     log.Default(),
    }
}

// 执行HTTP请求(带重试机制)
func (c *HTTPClient) DoWithRetry(req *http.Request) (*http.Response, error) {
    var resp *http.Response
    var err error

    for attempt := 0; attempt <= c.MaxRetries; attempt++ {
        resp, err = c.Client.Do(req)

        // 如果没有错误,检查状态码
        if err == nil {
            if resp.StatusCode < 400 {
                return resp, nil
            }

            // 非2xx状态码,根据状态码决定是否重试
            if c.shouldRetry(resp.StatusCode) {
                err = &HTTPError{
                    StatusCode: resp.StatusCode,
                    URL:        req.URL.String(),
                    Method:     req.Method,
                    Message:    "non-2xx status code",
                }
            } else {
                // 客户端错误不应重试
                return nil, &HTTPError{
                    StatusCode: resp.StatusCode,
                    URL:        req.URL.String(),
                    Method:     req.Method,
                    Message:    "client error",
                }
            }
        }

        // 记录错误
        c.Logger.Printf("Attempt %d failed: %v", attempt+1, err)

        // 最后一次尝试不再等待
        if attempt < c.MaxRetries {
            c.Logger.Printf("Retrying in %v...", c.RetryDelay)
            time.Sleep(c.RetryDelay)
        }
    }

    return nil, fmt.Errorf("after %d attempts: %w", c.MaxRetries+1, err)
}

// 根据状态码决定是否重试
func (c *HTTPClient) shouldRetry(statusCode int) bool {
    // 5xx服务器错误和429(太多请求)可以重试
    return statusCode >= 500 && statusCode < 600 || statusCode == 429
}

// 带超时的请求
func (c *HTTPClient) DoWithTimeout(ctx context.Context, req *http.Request, timeout time.Duration) (*http.Response, error) {
    timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
    defer cancel()

    req = req.WithContext(timeoutCtx)
    return c.DoWithRetry(req)
}

// 模拟网络错误
func simulateNetworkError() error {
    errors := []error{
        errors.New("connection refused"),
        errors.New("timeout"),
        &HTTPError{StatusCode: 500, Message: "internal server error"},
        &HTTPError{StatusCode: 429, Message: "too many requests"},
    }
    return errors[rand.Intn(len(errors))]
}

func main() {
    rand.Seed(time.Now().UnixNano())

    client := NewHTTPClient(3, 500*time.Millisecond)

    // 模拟请求
    req, err := http.NewRequest("GET", "https://example.com/api/data", nil)
    if err != nil {
        log.Fatal(err)
    }

    // 测试带重试的请求
    fmt.Println("Making request with retry mechanism...")

    // 在实际应用中,这里会调用client.DoWithRetry(req)
    // 为了演示,我们模拟网络错误

    // 模拟多个请求尝试
    for i := 0; i < 5; i++ {
        err := simulateNetworkError()
        fmt.Printf("Attempt %d: %v\n", i+1, err)

        if httpErr, ok := err.(*HTTPError); ok {
            if client.shouldRetry(httpErr.StatusCode) {
                fmt.Println("-> Retryable error")
            } else {
                fmt.Println("-> Non-retryable error")
                break
            }
        } else {
            fmt.Println("-> Network error, will retry")
        }

        if i < 4 {
            time.Sleep(500 * time.Millisecond)
        }
    }

    // 测试超时处理
    fmt.Println("\nTesting timeout handling...")
    ctx := context.Background()

    // 创建一个会超时的请求
    slowReq, err := http.NewRequest("GET", "https://httpbin.org/delay/10", nil)
    if err != nil {
        log.Fatal(err)
    }

    // 设置2秒超时
    _, err = client.DoWithTimeout(ctx, slowReq, 2*time.Second)
    if err != nil {
        fmt.Printf("Timeout error: %v\n", err)
    }
}

这份教程涵盖了Go语言错误处理的核心概念和最佳实践。通过理解和应用这些模式,你将能够构建更加健壮和可维护的Go应用程序。记住,良好的错误处理不仅仅是技术问题,更是设计哲学和用户体验的重要组成部分。


下节预告:反射机制与应用场景 - 探索Go语言的元编程能力