EmailVerify LogoEmailVerify

Gin

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

Integrate EmailVerify into your Gin applications for high-performance email validation. This guide covers middleware patterns, concurrent verification, and production deployment.

Installation

Install Gin framework and EmailVerify dependencies.

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
)

Configuration

Environment Setup with Viper

Configure your application using Viper for environment management:

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
}

.env File

Create a .env file for local development:

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

Email Verification Middleware

Create middleware for request validation and logging:

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()
    }
}

Rate Limiting Middleware

Implement rate limiting to prevent API abuse:

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()
    }
}

Handlers

Single Email Verification Handler

Create a handler for verifying individual emails:

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)
    }
}

Bulk Email Verification Handler

Create a handler for batch email verification:

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)
    }
}

Request Binding and Validation

Custom Validators

Extend validation with custom rules:

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)
}

Request Models with Validation

Define request models with validation tags:

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"`
}

Database Integration

GORM Models

Define database models for storing verification results:

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
}

Caching

Redis Caching Service

Implement Redis caching for verification results:

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 and Concurrency

Concurrent Verification

Verify emails concurrently using goroutines and worker pools:

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()
}

Error Handling

Error Response Models

Define consistent error handling:

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)
}

Testing

Table-Driven Tests

Test handlers and services with table-driven tests:

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
        })
    }
}

Production Setup

Graceful Shutdown

Implement graceful shutdown for production:

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")
}

Logging

Setup structured logging for production:

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)
}

Docker Deployment

Dockerfile

Create a multi-stage Dockerfile for production:

# 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"]

On this page