Shanraq.org Shanraq.org
Обработчик и middleware: одна обёртка на все маршруты
IT

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 == …, это уже не обёртка, а маршрутизатор, написанный второй раз и хуже. Такое место — либо отдельный обработчик, либо обёртка на один маршрут.

Карта урока

Карта урока: запрос проходит через обёртку дважды — туда и обратно

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Почему logging может обернуть mux, хотя mux — не функция?
  2. Наденьте две обёртки. Какая из них увидит запрос первой?
  3. Зачем обёртке свой recorder, если код ответа уже отправлен через w?

Задание

Обязательное. Напишите обёртку withHeader, которая добавляет в каждый ответ заголовок X-Blog: go-oqu, и наденьте её вместе с logging на весь маршрутизатор. Проверьте curl -i, что заголовок есть на всех трёх адресах, включая 404. Затем напишите вторую обёртку с сообщением до и после и убедитесь, что порядок именно тот, что вы предсказали.

По желанию.

  • Добавьте в recorder подсчёт байтов ответа и печатайте его в журнале.
  • Напишите обёртку recover, которая ловит панику обработчика и отвечает 500 вместо обрыва соединения.
  • Сделайте обёртку только для /slow — через mux.Handle, — и убедитесь, что на других адресах она не срабатывает.

Куда это встанет в блоге

Журнал вы только что написали — он останется до конца курса. К нему добавятся восстановление после паники и проверка входа, когда появятся пользователи.

А в следующем уроке в тело ответа наконец поедет HTML: шаблоны превратят []Article в страницу, и fmt.Fprintln из обработчиков уйдёт.

Ответы

Показать ответы
  1. Потому что обёртка принимает не функцию, а интерфейс http.Handler — всё, у чего есть метод ServeHTTP. У ServeMux он есть, поэтому маршрутизатор — такой же обработчик, как любой другой.
  2. Внешняя: она получает запрос первой и отдаёт ответ последней. Порядок как у матрёшек — что надето снаружи, то и встречает первым.
  3. Потому что ResponseWriter умеет только писать, а прочитать у него отправленный код нельзя. Обёртка подставляет свой recorder, который запоминает код по дороге и передаёт его настоящему писателю без изменений.

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.