错误处理的高级技巧:从基础到企业级

深入探讨 Go 错误处理的高级技巧,包括错误包装、错误链、自定义错误类型和企业级错误处理策略

错误处理的高级技巧:从基础到企业级

Go 的错误处理以其简洁性著称,但简洁不等于简单。在实际项目中,我们需要处理复杂的错误场景:错误包装、错误链、错误分类、错误监控等。

本文将带你从基础的错误处理进阶到企业级的错误处理策略。

回顾基础

基本的错误处理

package main

import (
    "errors"
    "fmt"
)

func divide(a, b int) (int, 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: %v\n", err)
        return
    }
    fmt.Printf("Result: %d\n", result)
}

这种基础模式适用于简单场景,但在复杂应用中远远不够。

错误包装与上下文

使用 fmt.Errorf 添加上下文

package main

import (
    "fmt"
    "os"
)

func readFile(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        // ❌ 不好:丢失了原始错误的上下文
        return nil, err
    }
    return data, nil
}

func readFileBetter(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        // ✅ 好:添加上下文信息
        return nil, fmt.Errorf("read file %s: %w", path, err)
    }
    return data, nil
}

func main() {
    _, err := readFileBetter("/nonexistent.txt")
    if err != nil {
        fmt.Println(err)
        // 输出:read file /nonexistent.txt: open /nonexistent.txt: no such file or directory
    }
}

多层包装

package main

import (
    "fmt"
    "os"
)

func loadConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("load config: %w", err)
    }
    return data, nil
}

func initApp(configPath string) error {
    _, err := loadConfig(configPath)
    if err != nil {
        return fmt.Errorf("init app: %w", err)
    }
    return nil
}

func main() {
    err := initApp("/nonexistent.conf")
    if err != nil {
        fmt.Println(err)
        // 输出:init app: load config: open /nonexistent.conf: no such file or directory
    }
}

错误链与错误检查

使用 errors.Is 检查错误

package main

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

func main() {
    _, err := os.Open("/nonexistent.txt")
    if err != nil {
        // ✅ 使用 errors.Is 检查特定错误
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("File does not exist")
        } else if errors.Is(err, os.ErrPermission) {
            fmt.Println("Permission denied")
        } else {
            fmt.Printf("Other error: %v\n", err)
        }
    }
}

使用 errors.As 提取错误类型

package main

import (
    "errors"
    "fmt"
    "net"
)

func connectToServer(addr string) error {
    _, err := net.Dial("tcp", addr)
    if err != nil {
        return fmt.Errorf("connect to %s: %w", addr, err)
    }
    return nil
}

func main() {
    err := connectToServer("invalid-address:9999")
    if err != nil {
        // ✅ 使用 errors.As 提取特定类型的错误
        var netErr net.Error
        if errors.As(err, &netErr) {
            fmt.Printf("Network error: timeout=%v, temporary=%v\n",
                netErr.Timeout(), netErr.Temporary())
        }
    }
}

自定义错误类型

基本的自定义错误

package main

import (
    "fmt"
)

// ValidationError 验证错误
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed: %s - %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 || age > 150 {
        return &ValidationError{
            Field:   "age",
            Message: "must be between 0 and 150",
        }
    }
    return nil
}

func main() {
    err := validateAge(200)
    if err != nil {
        fmt.Println(err)
        // 输出:validation failed: age - must be between 0 and 150
    }
}

带错误码的错误类型

package main

import (
    "fmt"
)

type ErrorCode int

const (
    ErrCodeNotFound ErrorCode = iota + 1
    ErrCodeInvalidInput
    ErrCodeUnauthorized
    ErrCodeInternal
)

type AppError struct {
    Code    ErrorCode
    Message string
    Err     error
}

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

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

// 构造函数
func NewNotFoundError(resource string) *AppError {
    return &AppError{
        Code:    ErrCodeNotFound,
        Message: fmt.Sprintf("%s not found", resource),
    }
}

func NewInvalidInputError(field, reason string) *AppError {
    return &AppError{
        Code:    ErrCodeInvalidInput,
        Message: fmt.Sprintf("invalid %s: %s", field, reason),
    }
}

func NewInternalError(err error) *AppError {
    return &AppError{
        Code:    ErrCodeInternal,
        Message: "internal server error",
        Err:     err,
    }
}

func main() {
    err := NewNotFoundError("user")
    fmt.Println(err)  // [1] user not found
    
    err2 := NewInvalidInputError("email", "invalid format")
    fmt.Println(err2)  // [2] invalid email: invalid format
}

错误集合

package main

import (
    "fmt"
    "strings"
)

type MultiError struct {
    Errors []error
}

func (m *MultiError) Error() string {
    if len(m.Errors) == 0 {
        return ""
    }
    
    var msgs []string
    for _, err := range m.Errors {
        msgs = append(msgs, err.Error())
    }
    return fmt.Sprintf("multiple errors: %s", strings.Join(msgs, "; "))
}

func (m *MultiError) Add(err error) {
    if err != nil {
        m.Errors = append(m.Errors, err)
    }
}

func (m *MultiError) HasErrors() bool {
    return len(m.Errors) > 0
}

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

func validateUser(name, email string, age int) error {
    var errs MultiError
    
    if name == "" {
        errs.Add(&ValidationError{"name", "cannot be empty"})
    }
    
    if email == "" {
        errs.Add(&ValidationError{"email", "cannot be empty"})
    }
    
    if age < 0 || age > 150 {
        errs.Add(&ValidationError{"age", "must be between 0 and 150"})
    }
    
    if errs.HasErrors() {
        return &errs
    }
    return nil
}

func main() {
    err := validateUser("", "", -1)
    if err != nil {
        fmt.Println(err)
        // 输出:multiple errors: name: cannot be empty; email: cannot be empty; age: must be between 0 and 150
    }
}

错误处理策略

策略 1:Fail Fast(快速失败)

package main

import (
    "errors"
    "fmt"
)

func processOrder(orderID string, amount float64) error {
    // ✅ 快速验证输入
    if orderID == "" {
        return errors.New("orderID cannot be empty")
    }
    if amount <= 0 {
        return errors.New("amount must be positive")
    }
    
    // 业务逻辑
    fmt.Printf("Processing order %s for $%.2f\n", orderID, amount)
    return nil
}

func main() {
    err := processOrder("", 100)
    if err != nil {
        fmt.Println(err)
        return
    }
}

策略 2:错误转换

package main

import (
    "database/sql"
    "errors"
    "fmt"
)

type Repository struct {
    db *sql.DB
}

type User struct {
    ID   int
    Name string
}

var ErrUserNotFound = errors.New("user not found")

func (r *Repository) GetUser(id int) (*User, error) {
    var user User
    err := r.db.QueryRow("SELECT id, name FROM users WHERE id = ?", id).
        Scan(&user.ID, &user.Name)
    
    if err == sql.ErrNoRows {
        // ✅ 将数据库错误转换为业务错误
        return nil, ErrUserNotFound
    }
    if err != nil {
        return nil, fmt.Errorf("query user: %w", err)
    }
    
    return &user, nil
}

策略 3:错误聚合与重试

package main

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

type RetryConfig struct {
    MaxAttempts int
    Delay       time.Duration
}

func withRetry(ctx context.Context, config RetryConfig, fn func() error) error {
    var lastErr error
    
    for attempt := 1; attempt <= config.MaxAttempts; attempt++ {
        err := fn()
        if err == nil {
            return nil
        }
        
        lastErr = err
        fmt.Printf("Attempt %d failed: %v\n", attempt, err)
        
        if attempt < config.MaxAttempts {
            select {
            case <-ctx.Done():
                return ctx.Err()
            case <-time.After(config.Delay):
                // 继续重试
            }
        }
    }
    
    return fmt.Errorf("all %d attempts failed, last error: %w",
        config.MaxAttempts, lastErr)
}

func unstableOperation() error {
    // 模拟不稳定的操作
    if rand.Float32() < 0.7 {
        return errors.New("temporary failure")
    }
    return nil
}

func main() {
    ctx := context.Background()
    config := RetryConfig{
        MaxAttempts: 3,
        Delay:       1 * time.Second,
    }
    
    err := withRetry(ctx, config, unstableOperation)
    if err != nil {
        fmt.Printf("Operation failed: %v\n", err)
    } else {
        fmt.Println("Operation succeeded")
    }
}

企业级错误处理

分层错误处理架构

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

// 1. 领域层错误
type DomainError struct {
    Code    string
    Message string
}

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

var (
    ErrUserNotFound    = &DomainError{"USER_NOT_FOUND", "User not found"}
    ErrInvalidPassword = &DomainError{"INVALID_PASSWORD", "Invalid password"}
)

// 2. 应用层错误
type AppError struct {
    DomainErr *DomainError
    Context   map[string]interface{}
    Err       error
}

func (e *AppError) Error() string {
    return fmt.Sprintf("%v (context: %v)", e.DomainErr, e.Context)
}

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

// 3. 传输层错误
type HTTPError struct {
    StatusCode int
    Code       string
    Message    string
    Details    interface{}
}

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

// 错误转换器
func toHTTPError(err error) *HTTPError {
    var appErr *AppError
    if errors.As(err, &appErr) {
        switch appErr.DomainErr.Code {
        case "USER_NOT_FOUND":
            return &HTTPError{
                StatusCode: http.StatusNotFound,
                Code:       appErr.DomainErr.Code,
                Message:    appErr.DomainErr.Message,
                Details:    appErr.Context,
            }
        case "INVALID_PASSWORD":
            return &HTTPError{
                StatusCode: http.StatusUnauthorized,
                Code:       appErr.DomainErr.Code,
                Message:    appErr.DomainErr.Message,
            }
        }
    }
    
    // 默认:内部错误
    return &HTTPError{
        StatusCode: http.StatusInternalServerError,
        Code:       "INTERNAL_ERROR",
        Message:    "An unexpected error occurred",
    }
}

// HTTP Handler
func handleError(w http.ResponseWriter, err error) {
    httpErr := toHTTPError(err)
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(httpErr.StatusCode)
    
    json.NewEncoder(w).Encode(map[string]interface{}{
        "error": map[string]interface{}{
            "code":    httpErr.Code,
            "message": httpErr.Message,
            "details": httpErr.Details,
        },
    })
}

错误监控与告警

package main

import (
    "fmt"
    "log"
    "os"
)

// 错误分类
type ErrorSeverity int

const (
    SeverityInfo ErrorSeverity = iota
    SeverityWarning
    SeverityError
    SeverityCritical
)

type MonitoredError struct {
    Err      error
    Severity ErrorSeverity
    Context  map[string]interface{}
}

// 错误监控器
type ErrorMonitor struct {
    logger *log.Logger
}

func NewErrorMonitor() *ErrorMonitor {
    return &ErrorMonitor{
        logger: log.New(os.Stdout, "[ERROR] ", log.LstdFlags),
    }
}

func (m *ErrorMonitor) Report(err *MonitoredError) {
    // 记录日志
    m.logger.Printf("[%v] %v (context: %v)",
        err.Severity, err.Err, err.Context)
    
    // 根据严重程度发送告警
    switch err.Severity {
    case SeverityCritical:
        m.sendAlert(err)
    case SeverityError:
        m.notifyTeam(err)
    }
}

func (m *ErrorMonitor) sendAlert(err *MonitoredError) {
    fmt.Printf("🚨 CRITICAL ALERT: %v\n", err.Err)
    // 发送短信、邮件、Slack 等
}

func (m *ErrorMonitor) notifyTeam(err *MonitoredError) {
    fmt.Printf("⚠️  Team notification: %v\n", err.Err)
    // 发送 Slack 消息等
}

// 使用示例
func processPayment(amount float64) error {
    if amount < 0 {
        return &MonitoredError{
            Err:      fmt.Errorf("invalid amount: %.2f", amount),
            Severity: SeverityWarning,
            Context: map[string]interface{}{
                "amount": amount,
            },
        }
    }
    
    // 模拟支付失败
    if amount > 10000 {
        return &MonitoredError{
            Err:      fmt.Errorf("payment gateway timeout"),
            Severity: SeverityCritical,
            Context: map[string]interface{}{
                "amount":      amount,
                "transaction": "TX-123",
            },
        }
    }
    
    return nil
}

func main() {
    monitor := NewErrorMonitor()
    
    err := processPayment(15000)
    if err != nil {
        if monErr, ok := err.(*MonitoredError); ok {
            monitor.Report(monErr)
        }
    }
}

错误处理中间件

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "runtime/debug"
    "time"
)

type contextKey string

const requestIDKey contextKey = "requestID"

// 错误处理中间件
func errorHandlingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 恢复 panic
        defer func() {
            if r := recover(); r != nil {
                // 记录堆栈
                stack := debug.Stack()
                fmt.Printf("Panic: %v\n%s\n", r, stack)
                
                // 返回 500 错误
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        
        // 添加超时
        ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
        defer cancel()
        
        // 添加 request ID
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = generateRequestID()
        }
        ctx = context.WithValue(ctx, requestIDKey, requestID)
        
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func generateRequestID() string {
    return fmt.Sprintf("req-%d", time.Now().UnixNano())
}

// 错误响应中间件
func errorResponseMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 包装 ResponseWriter 以捕获错误
        wrapped := &responseWriter{
            ResponseWriter: w,
            statusCode:     http.StatusOK,
        }
        
        next.ServeHTTP(wrapped, r)
        
        // 如果有错误,记录并返回标准格式
        if wrapped.statusCode >= 400 {
            requestID, _ := r.Context().Value(requestIDKey).(string)
            fmt.Printf("[%s] HTTP %d: %s %s\n",
                requestID, wrapped.statusCode, r.Method, r.URL.Path)
        }
    })
}

type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// 示例 Handler
func userHandler(w http.ResponseWriter, r *http.Request) {
    userID := r.URL.Query().Get("id")
    if userID == "" {
        http.Error(w, "missing user ID", http.StatusBadRequest)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{
        "id":   userID,
        "name": "John Doe",
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/user", userHandler)
    
    // 应用中间件
    handler := errorHandlingMiddleware(
        errorResponseMiddleware(mux),
    )
    
    http.ListenAndServe(":8080", handler)
}

错误处理最佳实践

1. 错误信息要清晰

// ❌ 不好:信息不明确
return errors.New("error")

// ✅ 好:信息清晰
return fmt.Errorf("failed to connect to database at %s: %w", dbHost, err)

2. 不要忽略错误

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

// ✅ 好:处理错误
result, err := doSomething()
if err != nil {
    return fmt.Errorf("do something: %w", err)
}

3. 使用 sentinel errors

// ✅ 定义 sentinel errors
var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrForbidden    = errors.New("forbidden")
)

// 使用时检查
if errors.Is(err, ErrNotFound) {
    // 处理 not found
}

4. 错误处理要幂等

// ✅ 幂等的错误处理
func cleanup() error {
    // 多次调用都是安全的
    os.Remove(tempFile)
    return nil
}

5. 记录错误上下文

// ✅ 记录完整的上下文
if err != nil {
    log.Printf("failed to process order: orderID=%s, userID=%s, err=%v",
        orderID, userID, err)
    return fmt.Errorf("process order %s: %w", orderID, err)
}

总结

错误处理是 Go 编程的核心技能:

基础技巧:

  1. 使用 fmt.Errorf 添加上下文
  2. 使用 %w 包装错误
  3. 使用 errors.Iserrors.As 检查错误

高级技巧:

  1. 自定义错误类型
  2. 错误集合
  3. 错误转换
  4. 错误聚合与重试

企业级策略:

  1. 分层错误处理架构
  2. 错误监控与告警
  3. 错误处理中间件
  4. 统一的错误响应格式

最佳实践:

  1. 错误信息要清晰
  2. 不要忽略错误
  3. 使用 sentinel errors
  4. 错误处理要幂等
  5. 记录错误上下文

记住:错误不是异常,而是值。正确处理错误,让你的程序更加健壮和可靠。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页