Middleware как Декоратор
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy. Урок 97 из 107.
Middleware — одно из наиболее практичных применений паттерна Decorator в Go. В веб-разработке функции middleware оборачивают обработчики HTTP, добавляя сквозные задачи, такие как ведение журналов, аутентификация или измерение времени выполнения, без изменения исходного обработчика.
Ключевая идея заключается в том, что middleware принимает handler и возвращает новый handler с добавленным поведением. Это в точности соответствует паттерну «Декоратор»: один и тот же интерфейс на входе и выходе, но с расширенной функциональностью:
type Handler func(request string) string
func LoggingMiddleware(next Handler) Handler {
return func(request string) string {
result := next(request)
return "[LOG] " + result
}
}
func AuthMiddleware(next Handler) Handler {
return func(request string) string {
return "[AUTH] " + next(request)
}
}Каждый middleware оборачивает следующий обработчик в цепочке, добавляя свое поведение до или после вызова обернутого обработчика. Вы можете объединить несколько middleware в стек, создав их композицию:
func MainHandler(request string) string {
return "Response to: " + request
}
handler := LoggingMiddleware(AuthMiddleware(MainHandler))
fmt.Println(handler("GET /users"))
// [LOG] [AUTH] Ответ на: GET /usersПорядок обёртывания определяет порядок выполнения: outermost middleware выполняет работу первым. Этот шаблон широко используется в веб-фреймворках Go для создания pipeline обработки request, что позволяет легко добавлять или удалять функциональность, не затрагивая основную логику handler.
Задание
ЛегкоДавайте создадим pipeline обработки request, используя паттерн Middleware как Decorator! Вы создадите систему, в которой функции middleware оборачивают обработчики, добавляя такие возможности, как измерение времени, валидация и форматирование — и всё это без изменения логики основного обработчика.
Вы организуете код в трёх файлах:
handler.go: Определите тип обработчика и базовый обработчик, который обрабатывает request.Создайте тип
Handlerкак функцию, которая принимает строку request и возвращает строку response:type Handler func(request string) stringРеализуйте функцию
BaseHandler, которая возвращаетProcessed: [request]для любого полученного request.middleware.go: Создайте функции middleware, которые оборачивают обработчики, добавляя дополнительное поведение.Создайте три функции middleware, каждая из которых принимает
Handlerи возвращает новыйHandler:TimingMiddleware: оборачивает response информацией о времени, возвращая[TIMING] [response]ValidationMiddleware: добавляет префикс валидации, возвращая[VALID] [response]UppercaseMiddleware: преобразует весь response в верхний регистр
Каждый middleware должен вызывать обёрнутый обработчик и улучшать его результат.
main.go: Создайте pipeline обработки, комбинируя middleware.Прочитайте количество слоёв middleware, которые нужно применить. Затем для каждого слоя прочитайте тип middleware (
timing,validationилиuppercase) и оберните текущий обработчик этим middleware. Применяйте их в порядке, в котором они были прочитаны: first прочитанный middleware становится внешней обёрткойoutermost.После создания pipeline прочитайте строку request, обработайте её с помощью составного обработчика и выведите результат.
Будут предоставлены следующие входные данные:
- Строка 1: количество слоёв middleware
- Следующие строки: по одному типу middleware в каждой строке (
timing,validationилиuppercase) - Последняя строка: request для обработки
Например, для следующих данных:
2
timing
validation
GET /usersВаш вывод должен быть таким:
[TIMING] [VALID] Processed: GET /usersА для следующих данных:
3
uppercase
timing
validation
POST /dataВаш вывод должен быть таким:
[TIMING] [VALID] PROCESSED: POST /DATAА для следующих данных:
1
uppercase
hello worldВаш вывод должен быть таким:
PROCESSED: HELLO WORLDА для следующих данных:
0
simple requestВаш вывод должен быть таким:
Processed: simple requestОбратите внимание, как порядок оборачивания middleware влияет на вывод: first применённый middleware (внешний, outermost) выполняется первым и видит конечный результат всех внутренних middleware. Middleware в верхнем регистре во втором примере выполняется last (внутренним), поэтому он переводит базовый response в верхний регистр до того, как timing и validation добавят свои префиксы!
Попробуйте сами
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Прочитать количество слоёв middleware
nLine, _ := reader.ReadString('\n')
n, _ := strconv.Atoi(strings.TrimSpace(nLine))
// Прочитать типы middleware
middlewareTypes := make([]string, n)
for i := 0; i < n; i++ {
line, _ := reader.ReadString('\n')
middlewareTypes[i] = strings.TrimSpace(line)
}
// Прочитать запрос
request, _ := reader.ReadString('\n')
request = strings.TrimSpace(request)
// TODO: Начать с BaseHandler
// TODO: Построить конвейер, оборачивая обработчик с помощью middleware
// Применять middleware по порядку — первый прочитанный middleware становится самой внешней обёрткой
// Hint: Пройти по middlewareTypes в ОБРАТНОМ порядке, чтобы добиться этого
// Последний применённый middleware будет самым внешним (выполняется первым)
// TODO: Обработать запрос через составленный обработчик и вывести результат
fmt.Println("TODO: implement pipeline")
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП в Go
Внешние файлыРабочее пространство и модули GoПакеты и импортыЭкспортируемые и неэкспортируемые именаВведение в ООП в GoСтруктуры как классыОпределение методов структурПолучатели-указатели и получатели-значенияИнициализация структурФункции-конструкторыИтоги — Простой калькулятор4Интерфейсы
Введение в интерфейсыНеявная реализацияИнтерфейс как контрактПустой интерфейс (any)Утверждение типаПереключатель типовКомпозиция интерфейсовИнтерфейсы Stringer и ErrorПовторение: Калькулятор фигур7Инкапсуляция
Экспортируемые и неэкспортируемые поляИнкапсуляция на уровне пакетовГеттеры и сеттерыСокрытие информации в GoИтоги — Записи о студентах10Обобщения (Generics) (Go 1.18+)
Введение в GenericsПараметры типовОграничения типовОбобщенные структурыОбходной путь для обобщенных методовИтоги — Обобщенная коллекция13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн Abstract FactoryПаттерн ObserverПаттерн Strategy2Глубокое погружение в типы и структуры
Базовые и составные типыОпределение пользовательских типовТеги структурАнонимные структурыВложенные структурыНулевые значения и значения по умолчаниюПовторение — Контактная книга5Композиция вместо наследования
Почему в Go нет наследованияОсновы встраивания структурПродвижение методовВстраивание нескольких структурВстраивание против агрегацииЗатенение встроенных методовИтоги — Иерархия сотрудников8Обработка ошибок и ООП
Интерфейс errorПользовательские типы ошибокОбертывание ошибок (fmt.Errorf)Sentinel-ошибкиerrors.Is() и errors.As()Panic, Defer и RecoverИтоги — Парсер файлов11Стандартная библиотека и ООП
io.Reader и io.Writersort.InterfaceИнтерфейс fmt.Stringerencoding/json со структурамиИнтерфейс http.HandlerПовторение: модели REST API14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикMiddleware как Декоратор3Указатели и память
Основы указателей в GoУказатели на структурыПередача по значению и по ссылкеФункция new()Сборка мусора в GoПовторение: Конструктор связного списка6Полиморфизм в Go
Полиморфизм через интерфейсыУтиная типизация в GoПравила реализации интерфейсовПолиморфные коллекцииВнедрение зависимостейИтоги — Обработчик платежей9Конкурентность и ООП
Основы горутинКаналы и взаимодействиеБуферизованные и небуферизованные каналыОператор selectsync.Mutex и sync.RWMutexsync.WaitGroupПроектирование потокобезопасных структурПовторение — Worker PoolПотренируйтесь самостоятельно: Онлайн-компилятор Go