Skip to content

集成服务错误处理

概述

集成服务错误处理规范定义了如何分类、转换和处理第三方服务的错误。核心目标是隐藏实现细节,提供统一的业务错误。

错误分类

错误类型表

错误类型说明处理方式
配置错误配置缺失或无效启动时失败,阻止应用启动
网络错误连接超时、DNS 解析失败重试,记录日志
认证错误API Key 无效、Token 过期立即失败,告警
业务错误文件不存在、格式错误、余额不足转换为业务错误返回
限流错误请求频率超限退避重试或返回错误

错误定义

定义业务错误常量

go
// clients/storage/errors.go
package storage

import "errors"

var (
    // 认证相关错误
    ErrStorageAccessDenied = errors.New("storage access denied")
    ErrStorageUnauthorized = errors.New("storage unauthorized")

    // 资源相关错误
    ErrStorageFileNotFound   = errors.New("storage file not found")
    ErrStorageBucketNotFound = errors.New("storage bucket not found")

    // 操作相关错误
    ErrStorageUploadFailed   = errors.New("storage upload failed")
    ErrStorageDownloadFailed = errors.New("storage download failed")
    ErrStorageDeleteFailed   = errors.New("storage delete failed")

    // 配置相关错误
    ErrInvalidConfig = errors.New("invalid storage config")
)

错误转换

转换原则

隐藏实现细节:不暴露第三方服务的内部错误 统一错误码:使用业务错误,而非第三方错误码

转换函数示例

go
// convertError 将第三方错误转换为业务错误
func (c *StorageClient) convertError(err error) error {
    if err == nil {
        return nil
    }

    // 根据错误类型转换
    var apiErr *s3types.APIError
    if errors.As(err, &apiErr) {
        switch apiErr.Code {
        case "AccessDenied":
            return ErrStorageAccessDenied
        case "NoSuchKey":
            return ErrStorageFileNotFound
        case "NoSuchBucket":
            return ErrStorageBucketNotFound
        case "InvalidAccessKeyId":
            return ErrStorageUnauthorized
        }
    }

    // 网络错误
    if isNetworkError(err) {
        return fmt.Errorf("storage network error: %w", err)
    }

    // 未知错误,包装原始错误
    return fmt.Errorf("%w: %v", ErrStorageUploadFailed, err)
}

// isNetworkError 判断是否为网络错误
func isNetworkError(err error) bool {
    if err == nil {
        return false
    }
    // 检查是否为超时错误
    if errors.Is(err, context.DeadlineExceeded) {
        return true
    }
    // 检查是否为连接错误
    if strings.Contains(err.Error(), "connection refused") {
        return true
    }
    return false
}

方法中使用错误转换

go
func (c *StorageClient) Upload(ctx context.Context, key string, data io.Reader) (*UploadResult, error) {
    // 调用第三方服务
    _, err := c.s3Client.PutObject(ctx, &s3.PutObjectInput{
        Bucket: aws.String(c.bucket),
        Key:    aws.String(key),
        Body:   data,
    })
    if err != nil {
        // ✅ 转换错误
        return nil, c.convertError(err)
    }

    return &UploadResult{Key: key}, nil
}

重试策略

指数退避重试

对于临时性错误(网络错误、限流),使用指数退避重试

go
import (
    "github.com/cenkalti/backoff/v4"
)

func (c *AIClient) GenerateWithRetry(ctx context.Context, prompt string) (*Response, error) {
    var response *Response

    operation := func() error {
        resp, err := c.generate(ctx, prompt)
        if err != nil {
            // 判断是否应该重试
            if isRetryableError(err) {
                return err  // 返回错误,触发重试
            }
            return backoff.Permanent(err)  // 不可重试错误
        }
        response = resp
        return nil
    }

    // 配置退避策略
    expBackoff := backoff.NewExponentialBackOff()
    expBackoff.MaxElapsedTime = 30 * time.Second

    // 执行重试
    if err := backoff.Retry(operation, expBackoff); err != nil {
        return nil, fmt.Errorf("ai generate failed after retry: %w", err)
    }

    return response, nil
}

// isRetryableError 判断错误是否可重试
func isRetryableError(err error) bool {
    // 网络错误可重试
    if isNetworkError(err) {
        return true
    }

    // 限流错误可重试
    if isRateLimitError(err) {
        return true
    }

    // 服务端错误(5xx)可重试
    if isServerError(err) {
        return true
    }

    return false
}

重试配置

go
type RetryConfig struct {
    MaxRetries  int           // 最大重试次数
    InitialWait time.Duration // 初始等待时间
    MaxWait     time.Duration // 最大等待时间
    Multiplier  float64       // 退避倍数
}

func DefaultRetryConfig() *RetryConfig {
    return &RetryConfig{
        MaxRetries:  3,
        InitialWait: 1 * time.Second,
        MaxWait:     30 * time.Second,
        Multiplier:  2.0,
    }
}

错误日志

记录错误日志

对于错误,记录详细日志便于排查

go
func (c *StorageClient) Upload(ctx context.Context, key string, data io.Reader) (*UploadResult, error) {
    _, err := c.s3Client.PutObject(ctx, &s3.PutObjectInput{
        Bucket: aws.String(c.bucket),
        Key:    aws.String(key),
        Body:   data,
    })
    if err != nil {
        // ✅ 记录详细错误日志
        logger.Errorw("storage upload failed",
            "key", key,
            "bucket", c.bucket,
            "error", err,
        )
        return nil, c.convertError(err)
    }

    logger.Infow("storage upload success", "key", key)
    return &UploadResult{Key: key}, nil
}

Service 层错误处理

Service 处理 Client 错误

go
func (s *ProductService) UploadProductImage(ctx context.Context, productID uint, imageData io.Reader) error {
    // 调用 StorageClient
    result, err := s.storageClient.Upload(ctx, fmt.Sprintf("products/%d.jpg", productID), imageData)
    if err != nil {
        // ✅ 根据不同错误返回不同消息
        if errors.Is(err, storage.ErrStorageAccessDenied) {
            return errors.New("存储服务权限不足")
        }
        if errors.Is(err, storage.ErrStorageUploadFailed) {
            return errors.New("图片上传失败,请稍后重试")
        }
        return fmt.Errorf("上传图片失败: %w", err)
    }

    // 更新商品图片 URL
    if err := s.db.Model(&models.Product{}).
        Where("id = ?", productID).
        Update("image_url", result.URL).Error; err != nil {
        return err
    }

    return nil
}

检查清单

  • [ ] 定义业务错误常量(如 ErrStorageUploadFailed
  • [ ] 实现错误转换函数 convertError()
  • [ ] 隐藏第三方服务的实现细节
  • [ ] 对于临时性错误实现重试机制
  • [ ] 记录详细错误日志
  • [ ] Service 层处理 Client 错误并返回用户友好消息

相关规范