集成服务错误处理
概述
集成服务错误处理规范定义了如何分类、转换和处理第三方服务的错误。核心目标是隐藏实现细节,提供统一的业务错误。
错误分类
错误类型表
| 错误类型 | 说明 | 处理方式 |
|---|---|---|
| 配置错误 | 配置缺失或无效 | 启动时失败,阻止应用启动 |
| 网络错误 | 连接超时、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 错误并返回用户友好消息
相关规范
- Client 层设计 - Client 方法中如何处理错误
- 错误处理规范 - 通用错误处理规范
- Service 层设计 - Service 错误处理