Skip to content

错误处理规范

核心原则

错误处理必须明确清晰,使用错误包装保留错误链,确保错误可追踪和调试。

允许的做法

错误定义

  • 使用 errors.New 定义固定错误
  • 使用 fmt.Errorf 创建动态错误
  • 使用 %w 包装错误
go
// ✅ 正确 - 定义错误
var (
    ErrUserNotFound     = errors.New("用户不存在")
    ErrEmailExists      = errors.New("邮箱已存在")
    ErrInvalidPassword  = errors.New("密码错误")
)

// ✅ 正确 - 创建错误
if user == nil {
    return fmt.Errorf("用户 ID %d 不存在", userID)
}

// ✅ 正确 - 包装错误
if err != nil {
    return fmt.Errorf("创建用户失败: %w", err)
}

错误判断

  • 使用 errors.Is 判断错误类型
  • 使用 errors.As 转换错误类型
go
// ✅ 正确 - 错误判断
if err != nil {
    if errors.Is(err, gorm.ErrRecordNotFound) {
        return errors.New("记录不存在")
    }
    return fmt.Errorf("查询失败: %w", err)
}

// ✅ 正确 - 错误转换
var validationErr *validator.ValidationErrors
if errors.As(err, &validationErr) {
    // 处理校验错误
}

Service 层错误处理

  • 返回 明确的错误
  • 包装 底层错误
go
// ✅ 正确 - Service 错误处理
func (s *UserService) Create(ctx context.Context, req *CreateUserRequest) (*models.User, error) {
    // 业务校验
    existingUser, err := s.findByEmail(ctx, req.Email)
    if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
        return nil, fmt.Errorf("查询邮箱失败: %w", err)
    }
    if existingUser != nil {
        return nil, errors.New("邮箱已存在")
    }

    // 创建用户
    user := &models.User{
        Name:      req.Name,
        Email:     req.Email,
        CreatedAt: time.Now().Unix(),
    }

    if err := s.db.WithContext(ctx).Create(user).Error; err != nil {
        return nil, fmt.Errorf("创建用户失败: %w", err)
    }

    return user, nil
}

Handler 层错误处理

  • 转换 错误为 HTTP 响应
  • 记录 错误日志
go
// ✅ 正确 - Handler 错误处理
func (h *UserHandler) Create(c echo.Context) error {
    user, err := h.userService.Create(c.Request().Context(), req)
    if err != nil {
        logger.Errorw("创建用户失败",
            "error", err,
            "email", req.Email,
        )

        // 根据错误类型返回不同响应
        if err.Error() == "邮箱已存在" {
            return c.JSON(400, response.Fail(3700, "邮箱已存在"))
        }

        return c.JSON(500, response.Fail(500, "创建用户失败"))
    }

    return c.JSON(201, response.Success(user))
}

日志记录

  • 使用 结构化日志
  • 记录 上下文信息
go
// ✅ 正确 - 结构化日志
import "code.hellotalk.com/infra/logger"

// 普通日志
logger.Infow("用户创建成功", "userID", user.ID, "email", user.Email)

// 错误日志
logger.Errorw("创建用户失败",
    "error", err,
    "email", req.Email,
    "requestID", requestID,
)

// 调试日志
logger.Debugw("处理请求", "requestID", requestID)

禁止的做法

  • 禁止 忽略错误
  • 禁止 使用 panic(除非不可恢复)
  • 避免 打印错误后继续执行
go
// ❌ 错误
user, _ := GetUser(id)  // 忽略错误

panic("error")  // 避免 panic

if err != nil {
    fmt.Println(err)  // 只打印不处理
}

// ✅ 正确
user, err := GetUser(id)
if err != nil {
    return fmt.Errorf("查询用户失败: %w", err)
}

特殊场景

事务错误处理

go
// ✅ 正确 - 事务错误处理
err := s.db.Transaction(func(tx *gorm.DB) error {
    if err := tx.Create(&user).Error; err != nil {
        return fmt.Errorf("创建用户失败: %w", err)
    }

    if err := tx.Create(&profile).Error; err != nil {
        return fmt.Errorf("创建资料失败: %w", err)
    }

    return nil
})

if err != nil {
    logger.Errorw("事务执行失败", "error", err)
    return fmt.Errorf("操作失败: %w", err)
}

参数校验错误

go
// ✅ 正确 - 参数校验错误处理
if err := c.Validate(req); err != nil {
    logger.Warnw("参数校验失败", "error", err)
    return c.JSON(400, response.Fail(1001, "参数格式错误"))
}

相关文档