net/http как фреймворк: Handler, ServeMux 1.22 и маршрутизация без зависимостей
http.Handler — один метод, HandlerFunc адаптирует обычные функции. С Go 1.22 ServeMux маршрутизирует по методу и шаблонам {id} с приоритетом более специфичного паттерна и паникой на конфликтах при регистрации. Большинству сервисов stdlib-маршрутизатора достаточно.
Аудит зависимостей выдал это в пятницу: библиотека-роутер, на которой держался платёжный API, заархивирована на GitHub — ни мейнтейнеров, ни реакции на CVE, 47 транзитивных пакетов. Команда заложила два спринта на миграцию на другой фреймворк. Потом кто-то честно посмотрел, что библиотека реально делала: path-параметры, матчинг по методу, хук NotFound. Всё. Вся эта поверхность была в стандартной библиотеке с Go 1.22. Миграция заняла один вечер — r.PathValue("id") вместо фреймворочной карты параметров, GET /users/{id} вместо цепочек билдера — и удалила 47 зависимостей из go.sum. Единственный настоящий баг оказался поучительным: два старых маршрута, которые молча перекрывали друг друга в порядке first-match-wins старого фреймворка, теперь заставили ServeMux паниковать на старте с конфликтом паттернов. Паника была способом stdlib сообщить о неоднозначности маршрутизации, которая ездила в прод два года.
Один интерфейс, один метод
Прежде чем тянуться за библиотекой-роутером, разберись, что stdlib уже даёт тебе — ответ изменит каждое последующее решение о зависимостях.
Весь HTTP-стек Go — роутеры, middleware, фреймворки, ваш код — говорит на одном контракте:
type Handler interface {
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
// HandlerFunc адаптирует обычную функцию к интерфейсу —
// это именованный функциональный тип с методом ServeHTTP, вызывающим саму функцию.
type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { f(w, r) }На HandlerFunc стоит задержаться: это функциональный тип, реализующий интерфейс, поэтому любая функция с нужной сигнатурой превращается в Handler без единой аллокации — mux.Handle("/x", http.HandlerFunc(myFunc)) или короче mux.HandleFunc("/x", myFunc). А ServeMux сам реализует Handler, поэтому роутеры вкладываются друг в друга: mux может быть хендлером внутри другого mux, middleware оборачивает mux так же, как одиночный эндпоинт. Эта однородность — причина, по которой экосистема Go так чисто композируется: нет фреймворочной сигнатуры хендлера, к которой нужен мост, есть один метод. Серверная сторона зеркальна: http.Server принимает любой Handler как корень; http.ListenAndServe(addr, mux) порождает по горутине на соединение, так что код хендлеров конкурентен по умолчанию и обязан быть к этому готов — обычная map, разделяемая хендлерами без мьютекса, становится гонкой данных на втором одновременном запросе.
ServeMux 1.22: методы, шаблоны, приоритет
До Go 1.22 stdlib-mux матчил только литеральные префиксы — ни методов, ни path-параметров — и именно эту дыру десятилетие закрывали сторонние роутеры. Паттерны 1.22 её закрыли:
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUser) // {id} матчит один сегмент
mux.HandleFunc("POST /users", createUser) // привязка к методу
mux.HandleFunc("GET /files/{path...}", serveFile) // {path...} матчит остаток
mux.HandleFunc("GET /{$}", home) // {$} = ровно "/", без поддерева
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // значение шаблона, без танцев с контекстом
// ...
}Правила, которые важны в проде:
- Паттерны с методом: паттерн с методом матчит только этот метод — кроме
GET, который матчит ещё иHEAD(сервер отбрасывает тело). Паттерн без метода матчит все методы. Запрос, совпавший по пути, но не по методу, автоматически получает405 Method Not Allowed. - Приоритет — у более специфичного паттерна, а не у зарегистрированного первым:
GET /users/newпобеждаетGET /users/{id}, а/users/{id}побеждает/users/. Порядок регистрации не важен — сознательное решение дизайна, чтобы поведение маршрута никогда молча не зависело от порядка init. - Конфликты — паника при регистрации: если два паттерна пересекаются и ни один не специфичнее (
GET /a/{x}/cпротивGET /a/b/{y}— оба матчат/a/b/c),Handleпаникует на старте. Громко и рано лучше, чем тихо и неправильно. - Паттерн с завершающим слэшем вроде
/static/матчит всё поддерево и даёт 301-редирект с/staticна/static/.
Сервис регистрирует сначала GET /users/{id}, потом GET /users/new. Приходит запрос GET /users/new. Какой хендлер сработает и почему?
▸Почему это работает
Почему паника на конфликте, а не просто выбор порядка? Потому что first-match-wins делает разрешение маршрутов зависимым от раскладки кода: перенесите регистрацию между файлами или поменяйте порядок init — и эндпоинт молча сменит хендлер; это ровно тот двухлетний латентный баг из приёма-крючка. Приоритет специфичности от порядка не зависит: один и тот же набор паттернов всегда разрешается одинаково, а оставшиеся неоднозначности — настоящие ничьи — падают быстро на старте, где их ловит пайплайн деплоя, а не в три ночи на живом запросе.
Когда фреймворк действительно нужен
Когда рука тянется к библиотеке-роутеру, спроси себя: что конкретно нужно такого, чего у mux нет — список окажется короче, чем кажется.
Будем честны в том, что сторонние веб-фреймворки добавляют поверх этого: биндинг и валидацию параметров, помощники content negotiation, экосистемы middleware, группы маршрутов. Ничего из этого не маршрутизация — mux 1.22 матчит запрос значительно быстрее микросекунды, что исчезает на фоне одного похода в базу за 200 мкс. Издержки фреймворка реальны и постоянны: нестандартная сигнатура хендлера, под которую приходится адаптировать каждый middleware, дерево зависимостей, которое вы аудируете вечно, и абстракция между вами и ResponseWriter ровно тогда, когда нужны Flusher или Hijacker. Прагматичная линия большинства Go-команд: начинать с net/http плюс тонкая цепочка middleware; брать библиотеку-роутер, только когда нужно то, чего у mux действительно нет (regex-ограничения, DSL для per-route middleware). Чего делать не стоит — криво написать валидацию руками и назвать это «без фреймворка»: берите сфокусированную библиотеку под сфокусированную задачу, а не фреймворк под всё.
Почему http.HandlerFunc(myFunc) удовлетворяет интерфейсу http.Handler, хотя myFunc — обычная функция?
- 01Объясни, как HandlerFunc превращает обычную функцию в http.Handler и почему вложенность ServeMux вытекает из того же дизайна.
- 02Опиши правила приоритета и конфликтов ServeMux 1.22 и режим отказа, который они должны были убить.
HTTP-стек Go глубиной в один интерфейс: единственный метод ServeHTTP — контракт и для эндпоинтов, и для роутеров, и для middleware, а HandlerFunc бесплатно адаптирует к нему любую функцию, потому что это функциональный тип со своим ServeHTTP. Сервер порождает горутину на соединение, так что код хендлеров конкурентен с первого деплоя, и разделяемое состояние требует синхронизации с первого дня. С Go 1.22 stdlib-ServeMux покрывает то, ради чего существовали сторонние роутеры: паттерны с методом, где GET обслуживает и HEAD, односегментные шаблоны {id}, читаемые через r.PathValue, {path...} для хвоста, {$} для точного корня и автоматический 405, когда путь совпал, а метод нет. Приоритет — у более специфичного паттерна, и он сознательно игнорирует порядок регистрации — GET /users/new бьёт GET /users/{id}, где бы он ни был объявлен, — а пересечения без победителя паникуют при регистрации, превращая тихое затенение маршрутов в падение на старте, которое ловит пайплайн. Матчинг паттерна стоит заметно меньше микросекунды — шум на фоне любой реальной работы хендлера. Фреймворки по-прежнему оправданы ради биндинга, валидации и экосистем middleware, но это эргономика, а не маршрутизация — а цена: своя сигнатура хендлера, вечный аудит зависимостей и прослойка перед ResponseWriter ровно там, где живут Flusher и Hijacker. Теперь, когда видишь запрос на добавление роутер-зависимости, сначала спроси: чего конкретно не хватает в mux? По умолчанию — stdlib; сфокусированные библиотеки — под сфокусированные задачи.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.