Gin Email Verification
Email checker for Go Gin. Email verification in Gin HTTP handlers.
Интегрируйте EmailVerify в ваши Gin приложения для высокопроизводительной валидации email. Это руководство охватывает паттерны middleware, конкурентную верификацию и развертывание в продакшен.
Установка
Установите Gin фреймворк и зависимости 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
)Настройка
Настройка окружения с Viper
Настройте приложение, используя Viper для управления окружением:
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 обязателен")
}
return cfg
}Файл .env
Создайте файл .env для локальной разработки:
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 для верификации email
Создайте middleware для валидации запросов и логирования:
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 - Статус: %d - Длительность: %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("Восстановление после паники: %v", err)
c.JSON(500, gin.H{"error": "Внутренняя ошибка сервера"})
}
}()
c.Next()
}
}Middleware для ограничения частоты запросов
Реализуйте ограничение частоты для предотвращения злоупотреблений 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": "Ошибка проверки лимита"})
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": "Слишком много запросов",
"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()
}
}Обработчики
Обработчик верификации одного email
Создайте обработчик для верификации отдельных email:
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": "Невалидный запрос",
"details": err.Error(),
})
return
}
result, cached, err := verificationService.VerifyEmail(c.Request.Context(), req.Email)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Верификация не удалась",
"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)
}
}Обработчик пакетной верификации email
Создайте обработчик для пакетной верификации email:
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": "Невалидный запрос",
"details": err.Error(),
})
return
}
startTime := time.Now()
// Ограничение результатов, если указано
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)
}
}Привязка запросов и валидация
Пользовательские валидаторы
Расширьте валидацию пользовательскими правилами:
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)
}Модели запросов с валидацией
Определите модели запросов с тегами валидации:
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"`
}Интеграция с базой данных
Модели GORM
Определите модели базы данных для хранения результатов верификации:
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
}
// Автоматическая миграция моделей
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
}Кэширование
Сервис кэширования Redis
Реализуйте кэширование Redis для результатов верификации:
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()
}Горутины и конкурентность
Конкурентная верификация
Верифицируйте email конкурентно, используя горутины и пулы воркеров:
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()
}Обработка ошибок
Модели ответов об ошибках
Определите единообразную обработку ошибок:
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)
}Тестирование
Table-Driven тесты
Тестирование обработчиков и сервисов с table-driven тестами:
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: "Валидный email",
email: "test@example.com",
expectedStatus: http.StatusOK,
expectedBody: map[string]interface{}{
"status": "valid",
},
},
{
name: "Невалидный email",
email: "invalid-email",
expectedStatus: http.StatusBadRequest,
expectedBody: map[string]interface{}{
"error": "Невалидный запрос",
},
},
{
name: "Пустой 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("Ожидался статус %d, получен %d", tt.expectedStatus, w.Code)
}
})
}
}
func TestBulkVerifyEmails(t *testing.T) {
tests := []struct {
name string
emails []string
expectedTotal int
expectedMinVerified int
}{
{
name: "Несколько валидных email",
emails: []string{"test1@example.com", "test2@example.com"},
expectedTotal: 2,
expectedMinVerified: 1,
},
{
name: "Пустой список",
emails: []string{},
expectedTotal: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Реализация теста
})
}
}Продакшен-настройка
Graceful Shutdown
Реализуйте graceful shutdown для продакшена:
package main
import (
"context"
"github.com/gin-gonic/gin"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
router := gin.Default()
// Регистрация маршрутов
setupRoutes(router)
server := &http.Server{
Addr: ":8000",
Handler: router,
}
// Запуск сервера в горутине
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Ошибка сервера: %v", err)
}
}()
// Ожидание сигнала прерывания
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Выключение сервера...")
// Graceful shutdown с таймаутом
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Сервер принудительно остановлен: %v", err)
}
log.Println("Сервер завершил работу")
}Логирование
Настройка структурированного логирования для продакшена:
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
Dockerfile
Создайте multi-stage Dockerfile для продакшена:
# Этап сборки
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 .
# Этап выполнения
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"]