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