EmailVerify LogoEmailVerify

Gin

Email checker for Go Gin. Email verification in Gin HTTP handlers.

Integre EmailVerify en sus aplicaciones Gin para validación de correo electrónico de alto rendimiento. Esta guía cubre patrones de middleware, verificación concurrente e implementación en producción.

Instalación

Instale el framework Gin y las dependencias de EmailVerify.

go get github.com/gin-gonic/gin
go get github.com/emailverify/emailverify-go
go get github.com/go-playground/validator/v10
go get github.com/go-redis/redis/v8
go get github.com/spf13/viper
go get gorm.io/gorm
go get gorm.io/driver/postgres
require (
    github.com/gin-gonic/gin v1.9.0
    github.com/emailverify/emailverify-go v0.1.0
    github.com/go-playground/validator/v10 v10.15.0
    github.com/go-redis/redis/v8 v8.11.5
    github.com/spf13/viper v1.16.0
    gorm.io/gorm v1.25.0
    gorm.io/driver/postgres v1.5.0
)

Configuración

Configuración del Entorno con Viper

Configure su aplicación usando Viper para la gestión del entorno:

package config

import (
    "log"
    "github.com/spf13/viper"
)

type Config struct {
    EmailVerifyAPIKey string
    DatabaseURL         string
    RedisURL            string
    ServerPort          string
    Environment         string
    RateLimit           int
    RateLimitWindow     int
}

func LoadConfig() *Config {
    viper.SetDefault("SERVER_PORT", "8000")
    viper.SetDefault("ENVIRONMENT", "development")
    viper.SetDefault("RATE_LIMIT", 100)
    viper.SetDefault("RATE_LIMIT_WINDOW", 3600)

    viper.BindEnv("EMAILVERIFY_API_KEY")
    viper.BindEnv("DATABASE_URL")
    viper.BindEnv("REDIS_URL")
    viper.BindEnv("SERVER_PORT")
    viper.BindEnv("ENVIRONMENT")

    cfg := &Config{
        EmailVerifyAPIKey: viper.GetString("EMAILVERIFY_API_KEY"),
        DatabaseURL:         viper.GetString("DATABASE_URL"),
        RedisURL:            viper.GetString("REDIS_URL"),
        ServerPort:          viper.GetString("SERVER_PORT"),
        Environment:         viper.GetString("ENVIRONMENT"),
        RateLimit:           viper.GetInt("RATE_LIMIT"),
        RateLimitWindow:     viper.GetInt("RATE_LIMIT_WINDOW"),
    }

    if cfg.EmailVerifyAPIKey == "" {
        log.Fatal("EMAILVERIFY_API_KEY is required")
    }

    return cfg
}

Archivo .env

Cree un archivo .env para desarrollo local:

EMAILVERIFY_API_KEY=your_api_key_here
DATABASE_URL=postgres://user:password@localhost/emailverify
REDIS_URL=redis://localhost:6379/0
SERVER_PORT=8000
ENVIRONMENT=development
RATE_LIMIT=100
RATE_LIMIT_WINDOW=3600

Middleware

Middleware de Verificación de Correo Electrónico

Cree middleware para validación de solicitudes y registro:

package middleware

import (
    "github.com/gin-gonic/gin"
    "log"
    "time"
)

func EmailVerificationLogging() gin.HandlerFunc {
    return func(c *gin.Context) {
        startTime := time.Now()

        c.Next()

        duration := time.Since(startTime)
        statusCode := c.Writer.Status()

        log.Printf(
            "[%s] %s %s - Status: %d - Duration: %v",
            time.Now().Format(time.RFC3339),
            c.Request.Method,
            c.Request.URL.Path,
            statusCode,
            duration,
        )
    }
}

func ErrorHandling() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("Panic recovered: %v", err)
                c.JSON(500, gin.H{"error": "Internal server error"})
            }
        }()

        c.Next()
    }
}

Middleware de Limitación de Tasa

Implemente limitación de tasa para prevenir el abuso de la API:

package middleware

import (
    "github.com/gin-gonic/gin"
    "github.com/go-redis/redis/v8"
    "context"
    "strconv"
    "time"
)

type RateLimiter struct {
    client *redis.Client
    limit  int
    window int
}

func NewRateLimiter(client *redis.Client, limit int, window int) *RateLimiter {
    return &RateLimiter{
        client: client,
        limit:  limit,
        window: window,
    }
}

func (rl *RateLimiter) Limit() gin.HandlerFunc {
    return func(c *gin.Context) {
        clientIP := c.ClientIP()
        key := "ratelimit:" + clientIP

        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()

        count, err := rl.client.Incr(ctx, key).Result()
        if err != nil {
            c.JSON(500, gin.H{"error": "Rate limit check failed"})
            c.Abort()
            return
        }

        if count == 1 {
            rl.client.Expire(ctx, key, time.Duration(rl.window)*time.Second)
        }

        if count > int64(rl.limit) {
            c.JSON(429, gin.H{
                "error": "Too many requests",
                "retry_after": rl.window,
            })
            c.Abort()
            return
        }

        c.Header("X-RateLimit-Limit", strconv.Itoa(rl.limit))
        c.Header("X-RateLimit-Remaining", strconv.Itoa(rl.limit - int(count)))

        c.Next()
    }
}

Manejadores

Manejador de Verificación de Correo Electrónico Individual

Cree un manejador para verificar correos electrónicos individuales:

package handlers

import (
    "github.com/gin-gonic/gin"
    "emailverify-app/services"
    "net/http"
)

type VerifyEmailRequest struct {
    Email string `json:"email" binding:"required,email"`
}

type VerifyEmailResponse struct {
    Email     string      `json:"email"`
    Status    string      `json:"status"`
    Score     float64     `json:"score"`
    Details   interface{} `json:"details"`
    Cached    bool        `json:"cached"`
    Timestamp string      `json:"timestamp"`
}

func VerifyEmail(verificationService *services.EmailVerificationService) gin.HandlerFunc {
    return func(c *gin.Context) {
        var req VerifyEmailRequest

        if err := c.ShouldBindJSON(&req); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{
                "error": "Invalid request",
                "details": err.Error(),
            })
            return
        }

        result, cached, err := verificationService.VerifyEmail(c.Request.Context(), req.Email)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{
                "error": "Verification failed",
                "details": err.Error(),
            })
            return
        }

        response := VerifyEmailResponse{
            Email:     req.Email,
            Status:    result.Status,
            Score:     result.Score,
            Details:   result.Details,
            Cached:    cached,
            Timestamp: result.Timestamp,
        }

        c.JSON(http.StatusOK, response)
    }
}

Manejador de Verificación Masiva de Correos Electrónicos

Cree un manejador para verificación de correos electrónicos en lote:

package handlers

import (
    "github.com/gin-gonic/gin"
    "emailverify-app/services"
    "net/http"
    "time"
)

type BulkVerifyRequest struct {
    Emails    []string `json:"emails" binding:"required,min=1"`
    MaxResults *int     `json:"max_results,omitempty"`
}

type BulkVerifyResponse struct {
    Total      int                  `json:"total"`
    Verified   int                  `json:"verified"`
    Failed     int                  `json:"failed"`
    Results    []VerifyEmailResponse `json:"results"`
    DurationMs float64              `json:"duration_ms"`
}

func BulkVerifyEmails(verificationService *services.EmailVerificationService) gin.HandlerFunc {
    return func(c *gin.Context) {
        var req BulkVerifyRequest

        if err := c.ShouldBindJSON(&req); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{
                "error": "Invalid request",
                "details": err.Error(),
            })
            return
        }

        startTime := time.Now()

        // Limit results if specified
        emails := req.Emails
        if req.MaxResults != nil && *req.MaxResults > 0 && *req.MaxResults < len(emails) {
            emails = emails[:*req.MaxResults]
        }

        results := make([]VerifyEmailResponse, 0, len(emails))
        failed := 0

        for _, email := range emails {
            result, cached, err := verificationService.VerifyEmail(c.Request.Context(), email)
            if err != nil {
                failed++
                continue
            }

            results = append(results, VerifyEmailResponse{
                Email:     email,
                Status:    result.Status,
                Score:     result.Score,
                Details:   result.Details,
                Cached:    cached,
                Timestamp: result.Timestamp,
            })
        }

        duration := time.Since(startTime)

        response := BulkVerifyResponse{
            Total:      len(emails),
            Verified:   len(results),
            Failed:     failed,
            Results:    results,
            DurationMs: duration.Seconds() * 1000,
        }

        c.JSON(http.StatusOK, response)
    }
}

Vinculación y Validación de Solicitudes

Validadores Personalizados

Extienda la validación con reglas personalizadas:

package validators

import (
    "github.com/go-playground/validator/v10"
    "strings"
)

type CustomValidator struct {
    validator *validator.Validate
}

func NewCustomValidator() *CustomValidator {
    v := validator.New()

    v.RegisterValidationFunc("not_disposable", func(fl validator.FieldLevel) bool {
        email := fl.Field().String()
        domain := strings.Split(email, "@")[1]

        blockedDomains := map[string]bool{
            "tempmail.com": true,
            "10minutemail.com": true,
            "guerrillamail.com": true,
        }

        return !blockedDomains[domain]
    })

    return &CustomValidator{validator: v}
}

func (cv *CustomValidator) Validate(i interface{}) error {
    return cv.validator.Struct(i)
}

Modelos de Solicitud con Validación

Defina modelos de solicitud con etiquetas de validación:

package models

type EmailVerificationRequest struct {
    Email string `json:"email" binding:"required,email,not_disposable"`
}

type BulkVerificationRequest struct {
    Emails []string `json:"emails" binding:"required,min=1,max=1000,dive,email"`
    Cache  bool     `json:"cache,omitempty"`
}

type VerificationQuery struct {
    CacheTTL int `form:"cache_ttl" binding:"omitempty,min=0,max=86400"`
}

Integración con Base de Datos

Modelos GORM

Defina modelos de base de datos para almacenar resultados de verificación:

package models

import (
    "gorm.io/gorm"
    "time"
)

type VerificationResult struct {
    ID            uint      `gorm:"primaryKey"`
    Email         string    `gorm:"uniqueIndex;not null;type:varchar(254)"`
    Status        string    `gorm:"not null;type:varchar(50)"`
    Score         float64
    Disposable    bool
    SmtpValid     bool
    FormatValid   bool
    EmailProvider string    `gorm:"type:varchar(100)"`
    RiskLevel     string    `gorm:"type:varchar(20)"`
    VerifiedAt    time.Time `gorm:"autoCreateTime"`
    CachedAt      time.Time `gorm:"autoUpdateTime"`
}

type Database struct {
    db *gorm.DB
}

func NewDatabase(dsn string) (*Database, error) {
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        return nil, err
    }

    // Auto migrate models
    if err := db.AutoMigrate(&VerificationResult{}); err != nil {
        return nil, err
    }

    return &Database{db: db}, nil
}

func (d *Database) SaveVerificationResult(email string, result interface{}) error {
    return d.db.Create(result).Error
}

func (d *Database) GetVerificationResult(email string) (*VerificationResult, error) {
    var result VerificationResult
    err := d.db.Where("email = ?", email).First(&result).Error
    return &result, err
}

Almacenamiento en Caché

Servicio de Almacenamiento en Caché con Redis

Implemente almacenamiento en caché con Redis para resultados de verificación:

package services

import (
    "github.com/go-redis/redis/v8"
    "context"
    "time"
    "encoding/json"
)

type CacheService struct {
    client *redis.Client
    ttl    time.Duration
}

func NewCacheService(client *redis.Client, ttl time.Duration) *CacheService {
    return &CacheService{
        client: client,
        ttl:    ttl,
    }
}

func (cs *CacheService) Get(ctx context.Context, key string, dest interface{}) error {
    val, err := cs.client.Get(ctx, key).Result()
    if err != nil {
        return err
    }

    return json.Unmarshal([]byte(val), dest)
}

func (cs *CacheService) Set(ctx context.Context, key string, value interface{}) error {
    data, err := json.Marshal(value)
    if err != nil {
        return err
    }

    return cs.client.Set(ctx, key, data, cs.ttl).Err()
}

func (cs *CacheService) Delete(ctx context.Context, key string) error {
    return cs.client.Del(ctx, key).Err()
}

func (cs *CacheService) Clear(ctx context.Context) error {
    return cs.client.FlushDB(ctx).Err()
}

Goroutines y Concurrencia

Verificación Concurrente

Verifique correos electrónicos de forma concurrente usando goroutines y pools de workers:

package services

import (
    "context"
    "sync"
)

type VerificationJob struct {
    Email string
    Done  chan VerificationResult
}

type VerificationWorkerPool struct {
    jobs   chan VerificationJob
    results chan VerificationResult
    wg     sync.WaitGroup
}

func NewVerificationWorkerPool(workerCount int, service *EmailVerificationService) *VerificationWorkerPool {
    pool := &VerificationWorkerPool{
        jobs:   make(chan VerificationJob, 100),
        results: make(chan VerificationResult, 100),
    }

    for i := 0; i < workerCount; i++ {
        pool.wg.Add(1)
        go pool.worker(context.Background(), service)
    }

    return pool
}

func (p *VerificationWorkerPool) worker(ctx context.Context, service *EmailVerificationService) {
    defer p.wg.Done()

    for job := range p.jobs {
        result, _, err := service.VerifyEmail(ctx, job.Email)
        if err != nil {
            job.Done <- VerificationResult{Error: err.Error()}
        } else {
            job.Done <- *result
        }
    }
}

func (p *VerificationWorkerPool) VerifyBatch(ctx context.Context, emails []string) []VerificationResult {
    results := make([]VerificationResult, len(emails))
    done := make(chan VerificationResult, len(emails))

    for _, email := range emails {
        job := VerificationJob{
            Email: email,
            Done:  done,
        }
        p.jobs <- job
    }

    for i := 0; i < len(emails); i++ {
        results[i] = <-done
    }

    return results
}

func (p *VerificationWorkerPool) Close() {
    close(p.jobs)
    p.wg.Wait()
}

Manejo de Errores

Modelos de Respuesta de Error

Defina manejo de errores consistente:

package models

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

type ErrorResponse struct {
    Error      string `json:"error"`
    Details    string `json:"details,omitempty"`
    StatusCode int    `json:"status_code"`
    Timestamp  string `json:"timestamp"`
}

func RespondError(c *gin.Context, statusCode int, message string, details string) {
    c.JSON(statusCode, ErrorResponse{
        Error:      message,
        Details:    details,
        StatusCode: statusCode,
        Timestamp:  time.Now().Format(time.RFC3339),
    })
}

func RespondSuccess(c *gin.Context, statusCode int, data interface{}) {
    c.JSON(statusCode, data)
}

Pruebas

Pruebas Basadas en Tablas

Pruebe manejadores y servicios con pruebas basadas en tablas:

package handlers

import (
    "testing"
    "bytes"
    "encoding/json"
    "github.com/gin-gonic/gin"
    "net/http"
    "net/http/httptest"
)

func TestVerifyEmail(t *testing.T) {
    tests := []struct {
        name           string
        email          string
        expectedStatus int
        expectedBody   map[string]interface{}
    }{
        {
            name:           "Valid email",
            email:          "test@example.com",
            expectedStatus: http.StatusOK,
            expectedBody: map[string]interface{}{
                "status": "valid",
            },
        },
        {
            name:           "Invalid email",
            email:          "invalid-email",
            expectedStatus: http.StatusBadRequest,
            expectedBody: map[string]interface{}{
                "error": "Invalid request",
            },
        },
        {
            name:           "Empty email",
            email:          "",
            expectedStatus: http.StatusBadRequest,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            router := gin.Default()

            reqBody := VerifyEmailRequest{Email: tt.email}
            body, _ := json.Marshal(reqBody)

            req := httptest.NewRequest("POST", "/verify", bytes.NewBuffer(body))
            req.Header.Set("Content-Type", "application/json")
            w := httptest.NewRecorder()

            router.ServeHTTP(w, req)

            if w.Code != tt.expectedStatus {
                t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
            }
        })
    }
}

func TestBulkVerifyEmails(t *testing.T) {
    tests := []struct {
        name            string
        emails          []string
        expectedTotal   int
        expectedMinVerified int
    }{
        {
            name:            "Multiple valid emails",
            emails:          []string{"test1@example.com", "test2@example.com"},
            expectedTotal:   2,
            expectedMinVerified: 1,
        },
        {
            name:            "Empty list",
            emails:          []string{},
            expectedTotal:   0,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Test implementation
        })
    }
}

Configuración de Producción

Apagado Gradual

Implemente apagado gradual para producción:

package main

import (
    "context"
    "github.com/gin-gonic/gin"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    router := gin.Default()

    // Register routes
    setupRoutes(router)

    server := &http.Server{
        Addr:    ":8000",
        Handler: router,
    }

    // Start server in goroutine
    go func() {
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()

    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    log.Println("Shutting down server...")

    // Graceful shutdown with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("Server forced to shutdown: %v", err)
    }

    log.Println("Server exited")
}

Registro

Configure el registro estructurado para producción:

package logging

import (
    "log/slog"
    "os"
)

func SetupLogger(environment string) {
    var level slog.Level
    if environment == "production" {
        level = slog.LevelInfo
    } else {
        level = slog.LevelDebug
    }

    logger := slog.New(
        slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
            Level: level,
        }),
    )

    slog.SetDefault(logger)
}

Implementación con Docker

Dockerfile

Cree un Dockerfile multi-etapa para producción:

# Build stage
FROM golang:1.21-alpine AS builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# Runtime stage
FROM alpine:latest

RUN apk --no-cache add ca-certificates

WORKDIR /root/

COPY --from=builder /app/main .

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD wget --quiet --tries=1 --spider http://localhost:8000/health || exit 1

CMD ["./main"]

Recursos Relacionados

On this page