open atlas
↑ К треку
Go с нуля до senior GO · 03 · 01

net/http как фреймворк: Handler, ServeMux 1.22 и маршрутизация без зависимостей

http.Handler — один метод, HandlerFunc адаптирует обычные функции. С Go 1.22 ServeMux маршрутизирует по методу и шаблонам {id} с приоритетом более специфичного паттерна и паникой на конфликтах при регистрации. Большинству сервисов stdlib-маршрутизатора достаточно.

GO Middle ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior
Уже знаешь этот юнит? Пройди быструю проверку за минуту →

Аудит зависимостей выдал это в пятницу: библиотека-роутер, на которой держался платёжный 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 — обычная функция?

Вспомните перед уходом
  1. 01
    Объясни, как HandlerFunc превращает обычную функцию в http.Handler и почему вложенность ServeMux вытекает из того же дизайна.
  2. 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.