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/postgresrequire (
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=3600Middleware
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"]