Роутинг и middleware в Go
Собрал цепочку Logging(Auth(handler)) и напечатал каждый шаг. Вывод получился такой: вход Logging, вход Auth, обработчик, выход Auth, выход Logging. Спрашивают именно это - как без фреймворка сделать сквозные вещи и в каком порядке они сработают.
Стержень: middleware принимает Handler и возвращает Handler, а порядок вложенности задаёт порядок выполнения.
// Формулировки: «как устроено middleware?», «в каком порядке они сработают?», «как залогировать статус ответа?»
Обёртка вокруг Handler
Middleware - функция вида func(next http.Handler) http.Handler. Она возвращает новый обработчик, который делает своё дело до вызова next.ServeHTTP и после него. Ни библиотеки, ни регистрации, ни магии.
Порядок из моего замера читается так: на пути запроса цепочка идёт снаружи внутрь, на пути ответа - обратно. Внешний слой видит запрос первым и ответ последним.
// Отсюда правило сборки. Восстановление после паники ставят самым внешним, чтобы оно поймало панику из любого слоя. Логирование - следом, чтобы в лог попал и запрос, который не прошёл аутентификацию. Аутентификацию - перед бизнес-логикой.
- middleware
- функция, оборачивающая Handler в новый Handler
- порядок цепочки
- снаружи внутрь на запросе, обратно на ответе
Статус ответа: почему нужна обёртка и чем она опасна
Интерфейс ResponseWriter умеет только писать. Прочитать уже отправленный статус он не даёт вовсе, поэтому логирующий слой подсовывает свою обёртку, которая запоминает код в WriteHeader и передаёт вызов дальше.
И тут ловушка, которую я проверил числом. Если обработчик не звал WriteHeader явно, статус проставится сам при первом Write. Обёртка этого не увидит: у меня в ней осталось 0, тогда как реальный статус был 200. Поэтому поле в обёртке инициализируют значением 200 при создании.
// Вторая опасность обёртки серьёзнее. Встроил ResponseWriter в свою структуру - и необязательные интерфейсы потерялись: у меня проверка на http.Flusher давала true у исходного writer и false у обёртки. Значит, сломается потоковая отдача, а с http.Hijacker - и веб-сокеты. Нужные интерфейсы приходится пробрасывать руками.
- обёртка ResponseWriter
- запоминает статус и размер ответа
- http.Flusher
- необязательный интерфейс, теряется при обёртке
Передача данных дальше
Данные для следующего обработчика - идентификатор пользователя после аутентификации, идентификатор трассировки - кладут в контекст: r = r.WithContext(context.WithValue(...)).
Ключ делают приватным типом своего пакета, обычно type ctxKey struct{}. Строковый ключ может совпасть с чужим из библиотеки, и такую перезапись потом почти не найти. Заголовки для этого не используют: они уедут наружу и подделываются клиентом.
// В контекст кладут только сквозные данные запроса. Обязательные параметры бизнес-логики должны быть видны в сигнатуре функции, иначе про них узнают в рантайме, по панике на приведении типа.
- приватный тип ключа
- защита от пересечения с чужим кодом
- сквозные данные
- трассировка и пользователь - то, что уместно в контексте
Как отвечать: «Как устроено middleware и в каком порядке выполняется цепочка?»
Middleware это обычная функция вида func(next http.Handler) http.Handler: она возвращает новый обработчик, который делает своё дело до вызова next.ServeHTTP и после него. Никакого фреймворка не нужно. Порядок я проверял печатью: цепочка Logging(Auth(handler)) даёт вход Logging, вход Auth, обработчик, выход Auth, выход Logging. То есть на пути запроса снаружи внутрь, на пути ответа обратно. Из этого следует сборка: восстановление после паники ставлю самым внешним, чтобы оно ловило панику из любого слоя, логирование следом, аутентификацию перед бизнес-логикой. Отдельная тонкость с логированием статуса: ResponseWriter умеет только писать, прочитать уже отправленный код нельзя, поэтому подсовываю свою обёртку с запоминанием в WriteHeader. И обязательно ставлю в ней 200 по умолчанию: если обработчик не звал WriteHeader, статус проставится сам при первом Write, а в обёртке останется ноль - я это специально воспроизводил. Ещё слежу, чтобы обёртка не убила необязательные интерфейсы: встроенный ResponseWriter не пробрасывает Flusher и Hijacker, и без ручного проброса ломается потоковая отдача и веб-сокеты. Данные дальше передаю через контекст с приватным типом ключа, а не через заголовки.
Сильный ответ: назван точный тип middleware, порядок разобран в обе стороны с практическим следствием для recover, объяснена необходимость обёртки вместе с ловушкой нулевого статуса, и добавлена потеря необязательных интерфейсов - её обычно вспоминают уже после инцидента со стримингом.
На чём валят
- −Путают порядок цепочки и ставят восстановление после паники не самым внешним.
- −Пытаются прочитать статус из ResponseWriter. Интерфейс этого не умеет.
- −Забывают выставить 200 по умолчанию в обёртке и пишут в лог нулевой статус.
- −Ломают обёрткой Flusher и Hijacker, а потом ищут, почему встал стриминг.
- −Передают данные через заголовки запроса вместо контекста.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 4, остальные разбираются в тренажёре.
- Как в net/http идиоматично устроено middleware (логирование, auth)?A)Через глобальные хуки, которые регистрируют в поле http.Server.OnRequestB)Функция, оборачивающая http.Handler в новый Handler с логикой до/послеC)Наследованием от базового http.BaseHandler с переопределением методаD)Отдельным потоком-перехватчиком перед каждым обработчиком
показать ответ и разбор
+B)Функция, оборачивающая http.Handler в новый Handler с логикой до/после// разбор: middleware в Go — функция func(next http.Handler) http.Handler: она возвращает новый Handler, который делает что-то ДО (лог, проверка токена), вызывает next.ServeHTTP и что-то ПОСЛЕ. Их выстраивают в цепочку, оборачивая друг в друга. Никаких глобальных хуков или наследования — только композиция мелкого интерфейса http.Handler.
- Хендлер обёрнут как Logging(Auth(handler)). В каком порядке отработает код на входящем запросе?A)handler → Auth → Logging: разворачивание идёт изнутри наружуB)Порядок недетерминирован, зависит от планировщика горутинC)Только Logging, если оно по какой-то причине явно не вызовет next.ServeHTTP каждый разD)Logging до, Auth до, handler, затем Auth и Logging на обратном пути
показать ответ и разбор
+D)Logging до, Auth до, handler, затем Auth и Logging на обратном пути// разбор: Обёртки выполняются снаружи внутрь на пути запроса и внутрь-наружу на обратном. Logging(Auth(handler)): сначала пред-код Logging, он зовёт next → пред-код Auth, тот зовёт next → handler; затем управление возвращается: пост-код Auth, потом пост-код Logging. Внешняя обёртка «обнимает» всё. Отсюда важен порядок: внешним ставят то, что должно видеть весь запрос (лог, recover).
- Middleware логирует статус ответа. Почему для этого приходится оборачивать ResponseWriter?A)Потому что интерфейс ResponseWriter не позволяет прочитать записанный статусB)Потому что статус доступен только после закрытия соединения клиентомC)Потому что обработчик может вообще не вызвать WriteHeaderD)Потому что оригинальный ResponseWriter не передают между функциями
показать ответ и разбор
+A)Потому что интерфейс ResponseWriter не позволяет прочитать записанный статус// разбор: Интерфейс умеет только писать: Header, Write, WriteHeader — прочитать, что уже отправлено, он не даёт. Поэтому в middleware подсовывают свою обёртку, которая запоминает код в WriteHeader и передаёт вызов дальше. Тонкость: если обработчик ничего не вызвал явно, статус будет 200 при первом Write — обёртка должна выставить это значение по умолчанию сама.
- Как middleware передаёт данные (например, id пользователя) следующему обработчику?A)Через глобальную map с ключом по адресу запросаB)Через заголовок запроса, дописанный в r.HeaderC)Через контекст запроса: r.WithContext с новым значениемD)Через поле структуры Server, общее для всех обработчиков
показать ответ и разбор
+C)Через контекст запроса: r.WithContext с новым значением// разбор: Идиома — положить значение в контекст и передать дальше запрос с обновлённым контекстом: r = r.WithContext(context.WithValue(...)). Ключ делают приватным типом своего пакета, иначе чужой код случайно перезапишет значение. Заголовки для этого не используют: они уедут наружу и их может подделать клиент, а глобальные map ломаются на конкурентности.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.