app.use(cors()) → corsMiddleware(next)
express-rate-limit → token bucket (своя реализация)
helmet → middleware безопасности (руками)
morgan → loggingMiddleware
express.json() → json.NewDecoder(r.Body)
express-validator → go-playground/validator (в этом уроке)
mkdir go-middleware && cd go-middleware
go mod init go-middleware
# Устанавливаем validator
go get github.com/go-playground/validator/v10
go get github.com/google/uuid
go mod tidy
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"runtime/debug"
"strings"
"sync"
"syscall"
"time"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ 1. ТИПЫ ДЛЯ КОНТЕКСТА ║
// ╚══════════════════════════════════════════════════════════╝
type contextKey string
const (
requestIDKey contextKey = "requestID"
loggerKey contextKey = "logger"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ 2. СТРУКТУРЫ ОТВЕТОВ ║
// ╚══════════════════════════════════════════════════════════╝
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
}
type ValidationError struct {
Field string `json:"field"`
Message string `json:"message"`
}
type ValidationErrorResponse struct {
Error string `json:"error"`
Fields []ValidationError `json:"fields"`
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 3. RATE LIMITER (token bucket) ║
// ╚══════════════════════════════════════════════════════════╝
// RateLimiter — простой token bucket rate limiter на IP
type RateLimiter struct {
mu sync.Mutex
buckets map[string]*bucket
rate int // токенов в секунду
burst int // максимальный всплеск
}
type bucket struct {
tokens float64
lastTime time.Time
}
// NewRateLimiter — rate токенов/сек, burst — размер ведра
func NewRateLimiter(rate, burst int) *RateLimiter {
rl := &RateLimiter{
buckets: make(map[string]*bucket),
rate: rate,
burst: burst,
}
// Очистка старых бакетов каждую минуту
go rl.cleanup()
return rl
}
// Allow — проверяет, можно ли выполнить запрос
func (rl *RateLimiter) Allow(key string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
b, ok := rl.buckets[key]
if !ok {
b = &bucket{
tokens: float64(rl.burst),
lastTime: time.Now(),
}
rl.buckets[key] = b
}
// Добавляем токены за прошедшее время
elapsed := time.Since(b.lastTime).Seconds()
b.tokens += elapsed * float64(rl.rate)
if b.tokens > float64(rl.burst) {
b.tokens = float64(rl.burst)
}
b.lastTime = time.Now()
// Проверяем, есть ли токен
if b.tokens >= 1 {
b.tokens--
return true
}
return false
}
// cleanup — удаляет старые бакеты
func (rl *RateLimiter) cleanup() {
for {
time.Sleep(time.Minute)
rl.mu.Lock()
for key, b := range rl.buckets {
if time.Since(b.lastTime) > 5*time.Minute {
delete(rl.buckets, key)
}
}
rl.mu.Unlock()
}
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 4. MIDDLEWARE ║
// ╚══════════════════════════════════════════════════════════╝
// Middleware — функция, принимающая http.Handler и возвращающая http.Handler
type Middleware func(http.Handler) http.Handler
// Chain — объединяет несколько middleware в цепочку
func Chain(middlewares ...Middleware) Middleware {
return func(next http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
next = middlewares[i](next)
}
return next
}
}
// ==========================================
// 4.1 Request ID Middleware
// ==========================================
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Проверяем, пришёл ли ID от клиента
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = uuid.New().String()
}
// Кладём в контекст
ctx := context.WithValue(r.Context(), requestIDKey, reqID)
// Возвращаем клиенту
w.Header().Set("X-Request-ID", reqID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// ==========================================
// 4.2 Logging Middleware
// ==========================================
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
reqID := GetRequestID(r.Context())
// Логируем запрос
log.Printf("[%s] → %s %s %s", reqID, r.Method, r.URL.Path, r.RemoteAddr)
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r)
// Логируем ответ
log.Printf("[%s] ← %d %s %s (%v)",
reqID, wrapped.statusCode, r.Method, r.URL.Path, time.Since(start))
})
}
// ==========================================
// 4.3 Recovery Middleware
// ==========================================
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
// Логируем панику и стек-трейс
reqID := GetRequestID(r.Context())
log.Printf("[%s] PANIC: %v\n%s", reqID, rec, debug.Stack())
// Возвращаем 500 клиенту
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(ErrorResponse{
Error: "internal_server_error",
Message: "An unexpected error occurred",
})
}
}()
next.ServeHTTP(w, r)
})
}
// ==========================================
// 4.4 CORS Middleware
// ==========================================
// CORSMiddleware — настраиваемый CORS
func CORSMiddleware(allowedOrigins []string, allowedMethods []string, allowedHeaders []string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// Проверяем, разрешён ли origin
allowed := false
for _, o := range allowedOrigins {
if o == "*" || o == origin {
allowed = true
break
}
}
if allowed {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", strings.Join(allowedMethods, ", "))
w.Header().Set("Access-Control-Allow-Headers", strings.Join(allowedHeaders, ", "))
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "86400") // 24 часа
}
// Preflight (OPTIONS) запрос
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
// ==========================================
// 4.5 Timeout Middleware
// ==========================================
func TimeoutMiddleware(timeout time.Duration) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
done := make(chan struct{})
tw := &timeoutWriter{ResponseWriter: w}
go func() {
next.ServeHTTP(tw, r.WithContext(ctx))
close(done)
}()
select {
case <-done:
// Завершилось нормально
case <-ctx.Done():
tw.timeout = true
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusGatewayTimeout)
json.NewEncoder(w).Encode(ErrorResponse{
Error: "timeout",
Message: "Request timed out",
})
}
})
}
}
// ==========================================
// 4.6 Rate Limit Middleware
// ==========================================
func RateLimitMiddleware(limiter *RateLimiter) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Используем IP как ключ
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
if !limiter.Allow(ip) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Retry-After", "1")
w.WriteHeader(http.StatusTooManyRequests)
json.NewEncoder(w).Encode(ErrorResponse{
Error: "rate_limit_exceeded",
Message: "Too many requests. Please try again later.",
})
return
}
next.ServeHTTP(w, r)
})
}
}
// ==========================================
// 4.7 Security Headers Middleware
// ==========================================
func SecurityHeadersMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Content-Security-Policy", "default-src 'self'")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
next.ServeHTTP(w, r)
})
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 5. ВСПОМОГАТЕЛЬНЫЕ ТИПЫ И ФУНКЦИИ ║
// ╚══════════════════════════════════════════════════════════╝
// responseWriter — обёртка для захвата статус-кода
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// timeoutWriter — отслеживает таймаут
type timeoutWriter struct {
http.ResponseWriter
timeout bool
}
func (tw *timeoutWriter) WriteHeader(code int) {
if !tw.timeout {
tw.ResponseWriter.WriteHeader(code)
}
}
func (tw *timeoutWriter) Write(b []byte) (int, error) {
if tw.timeout {
return 0, nil
}
return tw.ResponseWriter.Write(b)
}
// GetRequestID — извлекает request ID из контекста
func GetRequestID(ctx context.Context) string {
if id, ok := ctx.Value(requestIDKey).(string); ok {
return id
}
return "unknown"
}
// writeJSON — хелпер для JSON-ответов
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 6. ВАЛИДАЦИЯ С go-playground/validator ║
// ╚══════════════════════════════════════════════════════════╝
var validate = validator.New()
// CreateUserRequest — пример DTO с тегами валидации
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=50"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=150"`
Role string `json:"role" validate:"required,oneof=admin user moderator"`
}
// FormatValidationErrors — преобразует ошибки валидатора в читаемый формат
func FormatValidationErrors(err error) []ValidationError {
var validationErrors []ValidationError
if ve, ok := err.(validator.ValidationErrors); ok {
for _, fe := range ve {
validationErrors = append(validationErrors, ValidationError{
Field: fe.Field(),
Message: fmt.Sprintf("validation failed on '%s' tag", fe.Tag()),
})
}
}
return validationErrors
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 7. ХЕНДЛЕРЫ ║
// ╚══════════════════════════════════════════════════════════╝
func helloHandler(w http.ResponseWriter, r *http.Request) {
reqID := GetRequestID(r.Context())
writeJSON(w, http.StatusOK, map[string]any{
"message": "Hello, Gopher!",
"request_id": reqID,
})
}
func createUserHandler(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, ErrorResponse{
Error: "bad_request",
Message: "Invalid JSON format",
})
return
}
// Валидация
if err := validate.Struct(req); err != nil {
writeJSON(w, http.StatusUnprocessableEntity, ValidationErrorResponse{
Error: "validation_error",
Fields: FormatValidationErrors(err),
})
return
}
writeJSON(w, http.StatusCreated, map[string]any{
"message": "User created successfully",
"user": req,
})
}
func panicHandler(w http.ResponseWriter, r *http.Request) {
// Эта паника будет поймана RecoveryMiddleware
panic("something went terribly wrong!")
}
func slowHandler(w http.ResponseWriter, r *http.Request) {
// Демонстрация таймаута (если timeout < 2 секунд)
time.Sleep(2 * time.Second)
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 8. НАСТРОЙКА СЕРВЕРА ║
// ╚══════════════════════════════════════════════════════════╝
func setupRouter() http.Handler {
mux := http.NewServeMux()
// Публичные эндпоинты
mux.HandleFunc("GET /api/hello", helloHandler)
mux.HandleFunc("POST /api/users", createUserHandler)
// Демонстрационные эндпоинты
mux.HandleFunc("GET /api/panic", panicHandler) // Демонстрация recovery
mux.HandleFunc("GET /api/slow", slowHandler) // Демонстрация timeout
// Применяем middleware (порядок ВАЖЕН!)
rateLimiter := NewRateLimiter(10, 20) // 10 запросов/сек, burst 20
handler := Chain(
RecoveryMiddleware, // 1. Ловим паники (самый внешний)
RequestIDMiddleware, // 2. Генерируем request ID
LoggingMiddleware, // 3. Логируем
SecurityHeadersMiddleware, // 4. Заголовки безопасности
CORSMiddleware( // 5. CORS
[]string{"http://localhost:3000", "http://localhost:8080"},
[]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
[]string{"Content-Type", "Authorization", "X-Request-ID"},
),
RateLimitMiddleware(rateLimiter), // 6. Rate limiting
TimeoutMiddleware(5*time.Second), // 7. Таймаут запроса (самый внутренний)
)(mux)
return handler
}
// ╔══════════════════════════════════════════════════════════╗
// ║ MAIN ║
// ╚══════════════════════════════════════════════════════════╝
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Println("🚀 Запуск сервера с middleware...")
handler := setupRouter()
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
// Запуск
go func() {
log.Println("Сервер слушает на http://localhost:8080")
log.Println("Эндпоинты:")
log.Println(" GET /api/hello")
log.Println(" POST /api/users")
log.Println(" GET /api/panic (демонстрация recovery)")
log.Println(" GET /api/slow (демонстрация timeout)")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Ошибка сервера: %v", err)
}
}()
// Graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("🛑 Выключение сервера...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Ошибка при остановке: %v", err)
}
log.Println("✅ Сервер остановлен")
}
# Запуск сервера
go run main.go
# В другом терминале:
# 1. Обычный запрос (с request ID)
curl -v http://localhost:8080/api/hello
# 2. Создание пользователя (с валидацией)
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com","age":30,"role":"admin"}'
# 3. Ошибка валидации
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"A","email":"invalid","age":200,"role":"super"}'
# 4. CORS preflight
curl -X OPTIONS http://localhost:8080/api/hello \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: GET" \
-v
# 5. Проверка rate limiting (быстро отправить много запросов)
for i in {1..25}; do
curl -s http://localhost:8080/api/hello &
done
wait
# 6. Проверка recovery (паника)
curl http://localhost:8080/api/panic
# 7. Проверка таймаута
curl http://localhost:8080/api/slow
| Позиция | Middleware | Назначение |
|---|---|---|
| 1 (внешний) | Recovery | Ловит паники из ВСЕХ middleware и хендлеров |
| 2 | RequestID | Генерирует ID для логирования |
| 3 | Logging | Логирует запрос и ответ |
| 4 | SecurityHeaders | Добавляет защитные заголовки |
| 5 | CORS | Обрабатывает preflight и добавляет CORS-заголовки |
| 6 | RateLimit | Отклоняет запросы сверх лимита |
| 7 (внутренний) | Timeout | Ограничивает время выполнения запроса |
| — | Handler | Бизнес-логика |
go run main.go
# Сборка
go build -o middleware-demo main.go
./middleware-demo
💡 Best practices от сеньоров:
💡 Для Node.js разработчика:
(req, res, next) => {}. В Go — func(http.Handler) http.Handler.
next() — middleware явно оборачивает следующий обработчик.app.use().