MidТеория4 min

Decorator

Паттерн Decorator в Go: цепочки io.Reader/Writer, HTTP middleware и обёртки с дополнительным поведением

Decorator (Декоратор)

Проблема

Нужно добавить поведение к объекту динамически, не изменяя его код. Decorator оборачивает объект, сохраняя тот же интерфейс, но добавляя функциональность.

В Go Decorator -- один из самых естественных паттернов. Вся система io.Reader/io.Writer построена на нём.

Диаграмма

    Client --> LoggingReader --> GzipReader --> FileReader
                  |                 |              |
              (logs bytes)    (decompresses)    (reads file)

    All implement io.Reader:
    +------------+
    | io.Reader  |
    +------------+
    | Read(p)    |
    +------------+
          ^
    +-----+------+-----+
    |            |           |
    FileReader  GzipReader  LoggingReader
                  wraps-->     wraps-->

Реализация: io.Reader декораторы

package ioutil

import (
    "fmt"
    "io"
    "log/slog"
    "time"
)

// CountingReader counts the number of bytes read.
type CountingReader struct {
    reader    io.Reader
    BytesRead int64
}

func NewCountingReader(r io.Reader) *CountingReader {
    return &CountingReader{reader: r}
}

func (cr *CountingReader) Read(p []byte) (int, error) {
    n, err := cr.reader.Read(p)
    cr.BytesRead += int64(n)
    return n, err
}

// TimingReader measures how long reads take.
type TimingReader struct {
    reader   io.Reader
    Duration time.Duration
}

func NewTimingReader(r io.Reader) *TimingReader {
    return &TimingReader{reader: r}
}

func (tr *TimingReader) Read(p []byte) (int, error) {
    start := time.Now()
    n, err := tr.reader.Read(p)
    tr.Duration += time.Since(start)
    return n, err
}

// LoggingReader logs each read operation.
type LoggingReader struct {
    reader io.Reader
    logger *slog.Logger
    name   string
}

func NewLoggingReader(r io.Reader, logger *slog.Logger, name string) *LoggingReader {
    return &LoggingReader{reader: r, logger: logger, name: name}
}

func (lr *LoggingReader) Read(p []byte) (int, error) {
    n, err := lr.reader.Read(p)
    lr.logger.Debug("read",
        "source", lr.name,
        "bytes", n,
        "error", err,
    )
    return n, err
}

// LimitedReader limits total bytes that can be read.
type LimitedReader struct {
    reader    io.Reader
    remaining int64
}

func NewLimitedReader(r io.Reader, limit int64) *LimitedReader {
    return &LimitedReader{reader: r, remaining: limit}
}

func (lr *LimitedReader) Read(p []byte) (int, error) {
    if lr.remaining <= 0 {
        return 0, io.EOF
    }
    if int64(len(p)) > lr.remaining {
        p = p[:lr.remaining]
    }
    n, err := lr.reader.Read(p)
    lr.remaining -= int64(n)
    return n, err
}

Цепочка декораторов

func main() {
    file, err := os.Open("data.gz")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    // Build a decorator chain: file -> counting -> gzip -> logging
    counter := ioutil.NewCountingReader(file)
    gzReader, _ := gzip.NewReader(counter)
    timer := ioutil.NewTimingReader(gzReader)
    limited := ioutil.NewLimitedReader(timer, 1<<20) // 1MB limit

    // Client just uses io.Reader -- doesn't know about decorators
    data, err := io.ReadAll(limited)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Read %d bytes (compressed: %d) in %v\n",
        len(data), counter.BytesRead, timer.Duration)
}

Реализация: HTTP Middleware

HTTP middleware в Go -- это декоратор для http.Handler.

package middleware

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

// Logging decorates an http.Handler with request logging.
func Logging(logger *slog.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()

            // Wrap ResponseWriter to capture status code
            wrapped := &statusWriter{ResponseWriter: w, status: 200}

            next.ServeHTTP(wrapped, r)

            logger.Info("request",
                "method", r.Method,
                "path", r.URL.Path,
                "status", wrapped.status,
                "duration", time.Since(start),
            )
        })
    }
}

type statusWriter struct {
    http.ResponseWriter
    status int
}

func (w *statusWriter) WriteHeader(code int) {
    w.status = code
    w.ResponseWriter.WriteHeader(code)
}

// Recovery decorates a handler with panic recovery.
func Recovery(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                slog.Error("panic recovered", "error", err)
                http.Error(w, "Internal Server Error", 500)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// CORS adds Cross-Origin Resource Sharing headers.
func CORS(origin string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Access-Control-Allow-Origin", origin)
            w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
            w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

            if r.Method == http.MethodOptions {
                w.WriteHeader(http.StatusNoContent)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

Цепочка middleware

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

    // Chain decorators: CORS -> Logging -> Recovery -> Handler
    logger := slog.Default()
    handler := CORS("*")(Logging(logger)(Recovery(mux)))

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

Универсальная цепочка middleware

// Chain applies middlewares in order: first middleware is outermost.
func Chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
    // Apply in reverse so first middleware wraps outermost
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

// Usage
handler := Chain(mux,
    CORS("*"),
    Logging(logger),
    Recovery,
)

Примеры из стандартной библиотеки

// io package: decorators everywhere
bufio.NewReader(r)           // buffering decorator
io.LimitReader(r, n)         // limit decorator
io.TeeReader(r, w)           // tee decorator (reads + writes copy)
gzip.NewReader(r)            // decompression decorator

// http package
http.TimeoutHandler(h, d, msg) // timeout decorator
http.StripPrefix("/api", h)    // URL prefix decorator
http.MaxBytesHandler(h, n)     // body size limit decorator

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

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

  • Нужно добавить поведение к объекту динамически
  • Нужна комбинация поведений (логирование + метрики + сжатие)
  • Подкласс нежелателен (а в Go его и нет)
  • Поведение нужно включать/выключать в runtime

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

  • Достаточно одной обёртки (лишняя абстракция)
  • Порядок декораторов критичен и неочевиден
  • Декоратор должен знать о внутренностях обёрнутого объекта

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

Паттерн Отличие
Adapter Меняет интерфейс; Decorator сохраняет интерфейс
Proxy Контролирует доступ; Decorator добавляет поведение
Composite Организует дерево; Decorator добавляет обёртку

Проверь себя

Какой пакет стандартной библиотеки Go является классическим примером паттерна Decorator?

В чём ключевое требование паттерна Decorator?

Как HTTP middleware реализует паттерн Decorator в Go?