Go: с нуля до своего блога Урок 21 из 50
Обработчик и middleware: одна обёртка на все маршруты
Двадцать первый урок курса по Go. Обёртка вокруг маршрутизатора делает одно и то же для каждого запроса: журнал, время ответа, проверка. Что такое http.Handler, почему обёртка берёт обработчик и возвращает обработчик, и как узнать код ответа, которого ResponseWriter не хранит.
Зачем это нужно
Журнал нужен на каждой странице. Проверка входа — на половине. Замер времени — везде. Копировать три строки в каждый обработчик значит однажды забыть их в одном и полдня искать, почему «эта страница не пишется в лог».
Такие вещи пишут один раз и надевают на всё сразу.
Сразу целиком
Новая папка, go mod init sabaq19, main.go:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// logging заворачивает любой обработчик и пишет строку в журнал.
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &recorder{ResponseWriter: w, code: http.StatusOK}
next.ServeHTTP(rec, r)
log.Printf("%s %s → %d за %s", r.Method, r.URL.Path, rec.code, time.Since(start).Round(time.Millisecond))
})
}
// recorder запоминает код ответа: сам ResponseWriter его не хранит.
type recorder struct {
http.ResponseWriter
code int
}
func (rec *recorder) WriteHeader(code int) {
rec.code = code
rec.ResponseWriter.WriteHeader(code)
}
func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "главная")
}
func slow(w http.ResponseWriter, r *http.Request) {
time.Sleep(120 * time.Millisecond)
fmt.Fprintln(w, "медленная")
}
func gone(w http.ResponseWriter, r *http.Request) {
http.Error(w, "нет", http.StatusNotFound)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", home)
mux.HandleFunc("GET /slow", slow)
mux.HandleFunc("GET /gone", gone)
log.Println("http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", logging(mux)))
}
Запустите и сходите на все четыре адреса — /, /slow, /gone и несуществующий /kokek. Прежде чем смотреть ниже — скажите вслух, попадёт ли в журнал последний. Потом сверьте с терминалом:
GET / → 200 за 0s
GET /slow → 200 за 121ms
GET /gone → 404 за 0s
GET /kokek → 404 за 0s
Последняя строка — важная. /kokek не дошёл ни до одного вашего обработчика, 404 ответил сам маршрутизатор, — а в журнале запись есть.
Разбор
http.Handler — интерфейс с одним методом
type Handler interface {
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
Всё, что умеет ServeHTTP, — обработчик. И ServeMux тоже обработчик: у него есть этот метод, он смотрит на путь и зовёт нужную функцию.
Отсюда вся конструкция урока. Раз маршрутизатор — обработчик, его можно передать туда, где ждут обработчик. Например — в другой обработчик.
HandlerFunc — мостик от функции к интерфейсу
Ваши home и slow — обычные функции, у них нет метода ServeHTTP. Превращает их http.HandlerFunc: это тип с единственным методом, который просто вызывает саму функцию.
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ... })
Не пугайтесь записи: это преобразование типа, как int64(n). Функцию объявляют функцией, а в интерфейс её одевают этой строкой.
Middleware — функция, которая берёт обработчик и возвращает обработчик
func logging(next http.Handler) http.Handler
Вся суть в подписи. На входе — то, что было. На выходе — то же самое, но в обёртке. Поэтому обёртки можно надевать одну на другую, и ни маршрутизатор, ни обработчики о них не знают.
Внутри обёртки три части: что-то до, вызов next.ServeHTTP(w, r), что-то после.
Образ. Турникет на входе в здание. Он не знает, в какой кабинет вы идёте, и не решает за вас. Он отмечает время входа, пропускает и отмечает время выхода — одинаково для всех.
Порядок: обёртки надеваются как матрёшки
Наденем две и посмотрим, в каком порядке они сработают:
http.ListenAndServe(":8080", mark("внешняя")(mark("внутренняя")(mux)))
внешняя вошла
внутренняя вошла
обработчик
внутренняя вышла
внешняя вышла
Внешняя первой встречает запрос и последней провожает ответ. Отсюда простое правило: то, что должно случиться раньше всех, надевают последним, снаружи.
Образ. Матрёшки. Снимаете внешнюю — под ней следующая, и так до самой маленькой; собираете обратно в обратном порядке. Запрос проходит их насквозь и возвращается тем же путём.
Код ответа приходится запоминать самому
Вот место, где новички спотыкаются. У ResponseWriter нет способа спросить, какой код уже отправлен. Он умеет только записать.
Поэтому обёртка подсовывает свой:
type recorder struct {
http.ResponseWriter
code int
}
func (rec *recorder) WriteHeader(code int) {
rec.code = code
rec.ResponseWriter.WriteHeader(code)
}
Первая строка структуры — встраивание: recorder получает все методы ResponseWriter даром и остаётся полноценным ResponseWriter. Свой у него только один — WriteHeader, который запоминает код и передаёт дальше.
Оговорка. Этот
recorder— учебный. Настоящий умеет больше: не даёт записать код дважды (второйWriteHeaderв Go молча игнорируется, а знать об этом полезно) и передаёт дальше дополнительные уменияResponseWriter— напримерhttp.Flusher, без которого не работает потоковая отдача. Для журнала нашего хватает; для библиотеки — нет.
И code: http.StatusOK при создании — не украшение: обработчик, который просто пишет тело, не зовёт WriteHeader вовсе, а ответ всё равно уходит с кодом 200.
Образ. Копирка под бланком. Оригинал уходит адресату, копия остаётся у вас — и ничего не меняется в том, что получил адресат.
Обёртка на все маршруты и обёртка на один
http.ListenAndServe(":8080", logging(mux)) // на всё
mux.Handle("GET /admin", onlyStaff(adminPage)) // на один маршрут
Первое — общее для сайта: журнал, время, восстановление после паники. Второе — то, что нужно не всем: проверка входа, ограничение частоты запросов.
Чего в обёртку класть не надо
Обёртка соблазняет: она видит всё. Поэтому в ней быстро заводятся вещи, которым там не место.
Правило простое: обёртка делает одно и то же для всех. Как только внутри появляется if r.URL.Path == …, это уже не обёртка, а маршрутизатор, написанный второй раз и хуже. Такое место — либо отдельный обработчик, либо обёртка на один маршрут.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Почему
loggingможет обернутьmux, хотяmux— не функция? - Наденьте две обёртки. Какая из них увидит запрос первой?
- Зачем обёртке свой
recorder, если код ответа уже отправлен черезw?
Задание
Обязательное. Напишите обёртку withHeader, которая добавляет в каждый ответ заголовок X-Blog: go-oqu, и наденьте её вместе с logging на весь маршрутизатор. Проверьте curl -i, что заголовок есть на всех трёх адресах, включая 404. Затем напишите вторую обёртку с сообщением до и после и убедитесь, что порядок именно тот, что вы предсказали.
По желанию.
- Добавьте в
recorderподсчёт байтов ответа и печатайте его в журнале. - Напишите обёртку
recover, которая ловит панику обработчика и отвечает 500 вместо обрыва соединения. - Сделайте обёртку только для
/slow— черезmux.Handle, — и убедитесь, что на других адресах она не срабатывает.
Куда это встанет в блоге
Журнал вы только что написали — он останется до конца курса. К нему добавятся восстановление после паники и проверка входа, когда появятся пользователи.
А в следующем уроке в тело ответа наконец поедет HTML: шаблоны превратят []Article в страницу, и fmt.Fprintln из обработчиков уйдёт.
Ответы
Показать ответы
- Потому что обёртка принимает не функцию, а интерфейс
http.Handler— всё, у чего есть методServeHTTP. УServeMuxон есть, поэтому маршрутизатор — такой же обработчик, как любой другой. - Внешняя: она получает запрос первой и отдаёт ответ последней. Порядок как у матрёшек — что надето снаружи, то и встречает первым.
- Потому что
ResponseWriterумеет только писать, а прочитать у него отправленный код нельзя. Обёртка подставляет свойrecorder, который запоминает код по дороге и передаёт его настоящему писателю без изменений.
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.