Middleware как Декоратор
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 97 из 107.
Middleware — это одно из наиболее практичных применений паттерна Декоратор в Go. В веб-разработке функции middleware оборачивают HTTP-обработчики для добавления сквозной функциональности, такой как логирование, аутентификация или замер времени выполнения, без изменения исходного обработчика.
Ключевая идея заключается в том, что middleware принимает обработчик и возвращает новый обработчик с добавленным поведением. Это в точности соответствует паттерну Декоратор — тот же интерфейс на входе и выходе, но с расширенной функциональностью:
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Порядок обертывания определяет порядок выполнения — самое внешнее промежуточное ПО (middleware) запускается первым. Этот паттерн широко используется в веб-фреймворках на Go для конвейеров обработки запросов, что позволяет легко добавлять или удалять функциональность, не затрагивая основную логику обработчика.
Задание
ЛегкоДавайте создадим конвейер обработки запросов, используя паттерн Middleware как декоратор! Вы создадите систему, в которой функции промежуточного ПО (middleware) оборачивают обработчики для добавления такой функциональности, как замер времени, валидация и форматирование — и все это без изменения логики основного обработчика.
Вы организуете свой код в трех файлах:
handler.go: Определите тип обработчика и базовый обработчик, который обрабатывает запросы.Создайте тип
Handlerкак функцию, которая принимает строку запроса и возвращает строку ответа:type Handler func(request string) stringРеализуйте функцию
BaseHandler, которая возвращаетProcessed: [request]для любого полученного запроса.middleware.go: Создайте функции промежуточного ПО, которые оборачивают обработчики дополнительным поведением.Создайте три функции middleware, каждая из которых принимает
Handlerи возвращает новыйHandler:TimingMiddleware— оборачивает ответ информацией о времени, возвращая[TIMING] [response]ValidationMiddleware— добавляет префикс валидации, возвращая[VALID] [response]UppercaseMiddleware— преобразует весь ответ в верхний регистр
Каждое промежуточное ПО должно вызывать обернутый обработчик и дополнять его результат.
main.go: Постройте конвейер обработки путем композиции middleware.Прочитайте количество слоев middleware, которые нужно применить. Затем для каждого слоя прочитайте тип middleware (
timing,validationилиuppercase) и оберните текущий обработчик этим middleware. Применяйте их в том порядке, в котором они считываются — первое считанное middleware становится самой внешней оболочкой.После создания конвейера прочитайте строку запроса, обработайте ее через созданный обработчик и выведите результат.
Будут предоставлены следующие входные данные:
- Строка 1: Количество слоев middleware
- Следующие строки: По одному типу middleware на строку (
timing,validationилиuppercase) - Последняя строка: Запрос для обработки
Например, при вводе:
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 влияет на результат — первое примененное middleware (самое внешнее) запускается первым и видит окончательный результат от всех внутренних middleware. Второе middleware в примере с uppercase запускается последним (самым внутренним), поэтому оно переводит базовый ответ в верхний регистр до того, как timing и validation добавят свои префиксы!
Шпаргалка
Промежуточное ПО (Middleware) в Go следует паттерну «Декоратор», оборачивая обработчики для добавления функциональности без изменения исходного обработчика.
Определите тип обработчика как функцию:
type Handler func(request string) stringФункции Middleware принимают обработчик и возвращают новый обработчик с расширенным поведением:
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 в стек путем их композиции:
func MainHandler(request string) string {
return "Response to: " + request
}
handler := LoggingMiddleware(AuthMiddleware(MainHandler))
fmt.Println(handler("GET /users"))
// Output: [LOG] [AUTH] Response to: GET /usersСамый внешний middleware запускается первым, определяя порядок выполнения. Каждый middleware может добавлять поведение до или после вызова обернутого обработчика.
Попробуйте сами
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: Постройте конвейер (pipeline), обернув обработчик в 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