错误处理的高级技巧

掌握 Go 1.13+ 的错误包装、错误链和自定义错误类型,写出更优雅的错误处理代码

错误处理的高级技巧

如果你写过 Go 代码,一定对这样的代码不陌生:

result, err := doSomething()
if err != nil {
    return err
}

Go 的错误处理方式一直备受争议。有人认为它太啰嗦,有人认为它很清晰。不管你怎么看,Go 1.13 引入的错误包装机制让错误处理变得更加优雅和强大。

传统的错误处理

在 Go 1.13 之前,我们通常这样处理错误:

package main

import (
    "errors"
    "fmt"
)

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

func findUser(id int) (*User, error) {
    if id <= 0 {
        return nil, ErrNotFound
    }
    // 查找用户逻辑
    return &User{ID: id, Name: "Alice"}, nil
}

func main() {
    user, err := findUser(-1)
    if err != nil {
        if err == ErrNotFound {
            fmt.Println("User not found")
        } else {
            fmt.Printf("Unknown error: %v\n", err)
        }
        return
    }
    fmt.Printf("Found user: %s\n", user.Name)
}

这种方式的问题在于:当错误被层层传递时,我们会丢失上下文信息。

错误包装(Go 1.13+)

Go 1.13 引入了 fmt.Errorf%w 动词,允许我们包装错误并保留错误链:

package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("not found")

func findUser(id int) (*User, error) {
    if id <= 0 {
        return nil, fmt.Errorf("findUser: invalid id %d: %w", id, ErrNotFound)
    }
    return &User{ID: id}, nil
}

func getUser(id int) (*User, error) {
    user, err := findUser(id)
    if err != nil {
        return nil, fmt.Errorf("getUser: %w", err)
    }
    return user, nil
}

func main() {
    _, err := getUser(-1)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        // 输出: Error: getUser: findUser: invalid id -1: not found
        
        // 检查错误链
        if errors.Is(err, ErrNotFound) {
            fmt.Println("This is a not found error")
        }
    }
}

errors.Is 和 errors.As

errors.Is 用于检查错误链中是否包含特定错误:

if errors.Is(err, ErrNotFound) {
    // 处理 not found 错误
}

errors.As 用于检查错误链中是否包含特定类型的错误:

type ValidationError struct {
    Field   string
    Message string
}

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

func validateUser(user *User) error {
    if user.Name == "" {
        return &ValidationError{Field: "Name", Message: "cannot be empty"}
    }
    return nil
}

func main() {
    err := validateUser(&User{})
    if err != nil {
        var valErr *ValidationError
        if errors.As(err, &valErr) {
            fmt.Printf("Validation failed: %s - %s\n", valErr.Field, valErr.Message)
        }
    }
}

自定义错误类型

创建自定义错误类型可以让错误携带更多信息:

package main

import (
    "fmt"
    "net/http"
)

// APIError 表示 API 错误
type APIError struct {
    Code    int
    Message string
    Details map[string]interface{}
}

func (e *APIError) Error() string {
    return fmt.Sprintf("API error %d: %s", e.Code, e.Message)
}

// NewNotFoundError 创建 404 错误
func NewNotFoundError(resource string) *APIError {
    return &APIError{
        Code:    http.StatusNotFound,
        Message: fmt.Sprintf("%s not found", resource),
    }
}

// NewValidationError 创建 400 错误
func NewValidationError(field, message string) *APIError {
    return &APIError{
        Code:    http.StatusBadRequest,
        Message: "validation failed",
        Details: map[string]interface{}{
            "field":   field,
            "message": message,
        },
    }
}

// Handler 示例
func getUserHandler(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")
    if id == "" {
        err := NewValidationError("id", "is required")
        handleError(w, err)
        return
    }
    
    user, err := findUser(id)
    if err != nil {
        handleError(w, err)
        return
    }
    
    // 返回用户
    _ = user
}

func handleError(w http.ResponseWriter, err error) {
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        w.WriteHeader(apiErr.Code)
        json.NewEncoder(w).Encode(apiErr)
    } else {
        w.WriteHeader(http.StatusInternalServerError)
        json.NewEncoder(w).Encode(map[string]string{
            "error": "internal server error",
        })
    }
}

错误处理的最佳实践

1. 只在边界处处理错误

// 不好:在每一层都处理错误
func (s *Service) GetUser(id int) (*User, error) {
    user, err := s.repo.FindByID(id)
    if err != nil {
        log.Printf("Error finding user: %v", err)  // 这里不应该 log
        return nil, err
    }
    return user, nil
}

// 好:只在最外层处理
func (s *Service) GetUser(id int) (*User, error) {
    return s.repo.FindByID(id)
}

func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
    user, err := h.service.GetUser(id)
    if err != nil {
        log.Printf("Error getting user %d: %v", id, err)  // 在这里 log
        handleError(w, err)
        return
    }
    // 返回用户
}

2. 添加上下文信息

// 不好:丢失上下文
func processOrder(order *Order) error {
    err := validateOrder(order)
    if err != nil {
        return err
    }
    
    err = saveOrder(order)
    if err != nil {
        return err
    }
    
    return sendNotification(order)
}

// 好:添加上下文
func processOrder(order *Order) error {
    if err := validateOrder(order); err != nil {
        return fmt.Errorf("validate order %d: %w", order.ID, err)
    }
    
    if err := saveOrder(order); err != nil {
        return fmt.Errorf("save order %d: %w", order.ID, err)
    }
    
    if err := sendNotification(order); err != nil {
        return fmt.Errorf("send notification for order %d: %w", order.ID, err)
    }
    
    return nil
}

3. 使用多返回值而不是错误包装

// 不好:用错误表示不同的业务逻辑
func getUser(id int) (*User, error) {
    user := db.Find(id)
    if user == nil {
        return nil, errors.New("user not found")  // 这不是真正的错误
    }
    return user, nil
}

// 好:使用多返回值
func getUser(id int) (*User, bool, error) {
    user, err := db.Find(id)
    if err != nil {
        return nil, false, err  // 这是真正的错误
    }
    if user == nil {
        return nil, false, nil  // 用户不存在,但不是错误
    }
    return user, true, nil
}

4. 不要忽略错误

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

// 好:明确处理错误
result, err := riskyOperation()
if err != nil {
    // 处理错误
    return err
}

// 如果确实要忽略,使用 _ 并添加注释
result, _ = riskyOperation() // 忽略错误,因为...

实战:构建一个带重试的 HTTP 客户端

package main

import (
    "context"
    "errors"
    "fmt"
    "io"
    "net/http"
    "time"
)

// RetryError 表示重试错误
type RetryError struct {
    Attempts int
    LastErr  error
}

func (e *RetryError) Error() string {
    return fmt.Sprintf("failed after %d attempts: %v", e.Attempts, e.LastErr)
}

func (e *RetryError) Unwrap() error {
    return e.LastErr
}

// HTTPClient 带重试的 HTTP 客户端
type HTTPClient struct {
    client     *http.Client
    maxRetries int
    baseDelay  time.Duration
}

func NewHTTPClient(maxRetries int) *HTTPClient {
    return &HTTPClient{
        client:     &http.Client{Timeout: 30 * time.Second},
        maxRetries: maxRetries,
        baseDelay:  time.Second,
    }
}

func (c *HTTPClient) Get(ctx context.Context, url string) ([]byte, error) {
    var lastErr error
    
    for attempt := 0; attempt <= c.maxRetries; attempt++ {
        if attempt > 0 {
            // 指数退避
            delay := c.baseDelay * time.Duration(1<<uint(attempt-1))
            select {
            case <-time.After(delay):
            case <-ctx.Done():
                return nil, ctx.Err()
            }
        }
        
        req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
        if err != nil {
            return nil, fmt.Errorf("create request: %w", err)
        }
        
        resp, err := c.client.Do(req)
        if err != nil {
            lastErr = fmt.Errorf("attempt %d: %w", attempt+1, err)
            continue
        }
        defer resp.Body.Close()
        
        // 只有 5xx 错误才重试
        if resp.StatusCode >= 500 {
            lastErr = fmt.Errorf("attempt %d: server error %d", attempt+1, resp.StatusCode)
            continue
        }
        
        if resp.StatusCode >= 400 {
            return nil, fmt.Errorf("client error: %d", resp.StatusCode)
        }
        
        body, err := io.ReadAll(resp.Body)
        if err != nil {
            lastErr = fmt.Errorf("attempt %d: read body: %w", attempt+1, err)
            continue
        }
        
        return body, nil
    }
    
    return nil, &RetryError{
        Attempts: c.maxRetries + 1,
        LastErr:  lastErr,
    }
}

func main() {
    client := NewHTTPClient(3)
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    body, err := client.Get(ctx, "https://api.example.com/data")
    if err != nil {
        var retryErr *RetryError
        if errors.As(err, &retryErr) {
            fmt.Printf("Failed after %d attempts\n", retryErr.Attempts)
            fmt.Printf("Last error: %v\n", retryErr.LastErr)
        } else {
            fmt.Printf("Error: %v\n", err)
        }
        return
    }
    
    fmt.Printf("Success: %s\n", body)
}

总结

Go 的错误处理虽然看似啰嗦,但它强制我们显式处理每一个可能的错误,这让代码更加健壮。Go 1.13 的错误包装机制让错误处理变得更加优雅:

  • 使用 %w 包装错误,保留错误链
  • 使用 errors.Is 检查特定错误
  • 使用 errors.As 检查特定错误类型
  • 创建自定义错误类型携带更多信息
  • 只在边界处处理错误,中间层只包装和传递
  • 添加上下文信息,让错误更有意义

记住:错误是值,像处理其他值一样处理错误

继续阅读

探索更多技术文章

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

全部文章 返回首页