MidТеория4 min

Chain of Responsibility

Паттерн Chain of Responsibility в Go: цепочки обработчиков, HTTP middleware и конвейеры валидации

Chain of Responsibility (Цепочка обязанностей)

Проблема

Запрос должен быть обработан одним из нескольких обработчиков, но заранее неизвестно каким. Каждый обработчик решает: обработать запрос или передать дальше по цепочке. Это позволяет настраивать цепочку динамически.

В Go этот паттерн повсюду: HTTP middleware -- классический Chain of Responsibility.

Диаграмма

    Request --> Auth --> RateLimit --> Validation --> Handler
                 |          |             |
                401        429           400       (если не прошёл)
                           |
                     next(request)       (если прошёл -- передать дальше)

Реализация: Цепочка обработчиков

package chain

import "fmt"

// Request represents a generic request.
type Request struct {
    UserID    string
    Action    string
    Resource  string
    IPAddress string
    Token     string
    Body      map[string]any
}

// Response is the handler result.
type Response struct {
    Allowed bool
    Reason  string
}

// Handler processes a request or passes it to the next handler.
type Handler interface {
    Handle(req Request) Response
    SetNext(handler Handler) Handler
}

// BaseHandler provides default chaining behavior.
type BaseHandler struct {
    next Handler
}

func (h *BaseHandler) SetNext(handler Handler) Handler {
    h.next = handler
    return handler
}

func (h *BaseHandler) HandleNext(req Request) Response {
    if h.next != nil {
        return h.next.Handle(req)
    }
    return Response{Allowed: true, Reason: "passed all checks"}
}

Конкретные обработчики

// AuthHandler checks if the request has a valid token.
type AuthHandler struct {
    BaseHandler
    validTokens map[string]string // token -> userID
}

func NewAuthHandler(tokens map[string]string) *AuthHandler {
    return &AuthHandler{validTokens: tokens}
}

func (h *AuthHandler) Handle(req Request) Response {
    if req.Token == "" {
        return Response{Allowed: false, Reason: "missing auth token"}
    }
    if _, ok := h.validTokens[req.Token]; !ok {
        return Response{Allowed: false, Reason: "invalid auth token"}
    }
    return h.HandleNext(req)
}

// RateLimitHandler limits requests per IP.
type RateLimitHandler struct {
    BaseHandler
    counts map[string]int
    limit  int
}

func NewRateLimitHandler(limit int) *RateLimitHandler {
    return &RateLimitHandler{
        counts: make(map[string]int),
        limit:  limit,
    }
}

func (h *RateLimitHandler) Handle(req Request) Response {
    h.counts[req.IPAddress]++
    if h.counts[req.IPAddress] > h.limit {
        return Response{
            Allowed: false,
            Reason:  fmt.Sprintf("rate limit exceeded for IP %s", req.IPAddress),
        }
    }
    return h.HandleNext(req)
}

// PermissionHandler checks if user can perform the action.
type PermissionHandler struct {
    BaseHandler
    permissions map[string][]string // userID -> allowed actions
}

func NewPermissionHandler(perms map[string][]string) *PermissionHandler {
    return &PermissionHandler{permissions: perms}
}

func (h *PermissionHandler) Handle(req Request) Response {
    actions, ok := h.permissions[req.UserID]
    if !ok {
        return Response{Allowed: false, Reason: "no permissions defined"}
    }
    for _, a := range actions {
        if a == req.Action || a == "*" {
            return h.HandleNext(req)
        }
    }
    return Response{
        Allowed: false,
        Reason:  fmt.Sprintf("user %s cannot %s", req.UserID, req.Action),
    }
}

Сборка цепочки

func main() {
    auth := NewAuthHandler(map[string]string{
        "token-123": "alice",
        "token-456": "bob",
    })

    rateLimit := NewRateLimitHandler(100)

    perms := NewPermissionHandler(map[string][]string{
        "alice": {"read", "write", "delete"},
        "bob":   {"read"},
    })

    // Build the chain
    auth.SetNext(rateLimit).SetNext(perms)

    // Test requests
    req := Request{
        UserID:    "alice",
        Action:    "write",
        Token:     "token-123",
        IPAddress: "192.168.1.1",
    }

    resp := auth.Handle(req)
    fmt.Printf("Allowed: %v, Reason: %s\n", resp.Allowed, resp.Reason)
}

Идиоматичный Go: middleware функции

В Go более распространён функциональный подход без SetNext():

package middleware

import (
    "log/slog"
    "net/http"
    "time"
)

// Middleware is a function that wraps an http.Handler.
type Middleware func(http.Handler) http.Handler

// Chain applies middlewares in order.
// First middleware is the outermost (executed first).
func Chain(h http.Handler, mws ...Middleware) http.Handler {
    for i := len(mws) - 1; i >= 0; i-- {
        h = mws[i](h)
    }
    return h
}

// RequestID adds a request ID to each request.
func RequestID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := fmt.Sprintf("req-%d", time.Now().UnixNano())
        r.Header.Set("X-Request-ID", id)
        w.Header().Set("X-Request-ID", id)
        next.ServeHTTP(w, r)
    })
}

// Logger logs each request.
func Logger(logger *slog.Logger) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            next.ServeHTTP(w, r)
            logger.Info("request",
                "method", r.Method,
                "path", r.URL.Path,
                "duration", time.Since(start),
                "request_id", r.Header.Get("X-Request-ID"),
            )
        })
    }
}

// Auth checks for authorization header.
func Auth(secret string) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            token := r.Header.Get("Authorization")
            if token != "Bearer "+secret {
                http.Error(w, "Unauthorized", http.StatusUnauthorized)
                return // stop the chain
            }
            next.ServeHTTP(w, r)
        })
    }
}

Использование:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /api/data", handleData)

    handler := middleware.Chain(mux,
        middleware.RequestID,
        middleware.Logger(slog.Default()),
        middleware.Auth("my-secret"),
    )

    http.ListenAndServe(":8080", handler)
}

Конвейер валидации

package validation

import "fmt"

// Validator validates a value and returns an error if invalid.
type Validator func(value any) error

// Chain runs validators in sequence, stopping at first error.
func Chain(validators ...Validator) Validator {
    return func(value any) error {
        for _, v := range validators {
            if err := v(value); err != nil {
                return err
            }
        }
        return nil
    }
}

// NotEmpty checks that string is not empty.
func NotEmpty(field string) Validator {
    return func(value any) error {
        s, ok := value.(string)
        if !ok || s == "" {
            return fmt.Errorf("%s is required", field)
        }
        return nil
    }
}

// MinLength checks minimum string length.
func MinLength(field string, min int) Validator {
    return func(value any) error {
        s, _ := value.(string)
        if len(s) < min {
            return fmt.Errorf("%s must be at least %d characters", field, min)
        }
        return nil
    }
}

Когда использовать

Используйте, когда:

  • Несколько обработчиков для одного запроса
  • Порядок и состав обработчиков настраивается динамически
  • HTTP middleware pipeline
  • Конвейеры валидации

Не используйте, когда:

  • Обработчик всегда один (просто вызовите его)
  • Порядок фиксирован и прост (обычная последовательность вызовов)

Сравнение с похожими паттернами

Паттерн Отличие
Decorator Все декораторы выполняются; в CoR цепочка может прерваться
Command Инкапсулирует действие; CoR передаёт запрос по цепочке
Mediator Централизованная координация; CoR -- последовательная

Проверь себя

Какой механизм Go является классическим примером Chain of Responsibility?

Какой подход к Chain of Responsibility более идиоматичен в Go?

Чем отличается CoR от Decorator?