Маршрутизация в Go: что советуют на практике
После стандартного роутера в net/http из Go 1.22 выбор библиотеки перестал быть очевидным. Разбираем, когда хватает stdlib, а когда нужен chi или gin, и чем платишь за каждый вариант.

В Go 1.22 (февраль 2024) стандартный http.ServeMux научился разбирать методы и path-параметры прямо в шаблоне маршрута: GET /users/{id} теперь валидный паттерн. Раньше это была главная причина тащить в проект gorilla/mux или chi. Теперь причина ослабла, и на code review всё чаще спорят: нужна ли вообще внешняя библиотека роутинга. Разбираем, что выбирать под конкретную задачу и чем платишь за каждое решение.
Что изменилось в стандартной библиотеке
До 1.22 ServeMux умел только префиксный матчинг: /users/ ловил всё, что начинается с этой строки, а метод и id вы разбирали руками. Приходилось либо писать простыню из if r.Method != http.MethodGet, либо ставить роутер.
С 1.22 паттерн может содержать метод и именованные сегменты. Пример на чистом stdlib:
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
// метод + path-параметр прямо в шаблоне
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // извлекаем сегмент по имени
fmt.Fprintf(w, "user id=%s", id)
})
// wildcard в конце: ловит остаток пути
mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "path=%s", r.PathValue("path"))
})
http.ListenAndServe(":8080", mux)
}
Метод r.PathValue появился именно в 1.22 — на более старых версиях кода выше не будет: пакет net/http просто не знает такого метода. Приоритет маршрутов при пересечении шаблонов теперь тоже определён: более специфичный паттерн выигрывает у более общего.
Чего stdlib по-прежнему не умеет
Обновлённый ServeMux закрывает базовый случай, но не превращается в полноценный роутер:
- Группы маршрутов и общий префикс. Нет способа сказать «все эти хендлеры живут под
/api/v1и делят одну цепочку middleware». Придётся дублировать префикс в каждом паттерне. - Middleware из коробки. Оборачивать хендлеры вы можете и сами, но композиция цепочки (логирование, recover, auth) остаётся ручной.
- Ограничения на значение параметра. Нельзя сказать «id — только цифры»: регэкспов в шаблоне нет, валидацию пишете внутри хендлера.
- Method Not Allowed. Если путь есть, а метод не совпал, поведение не всегда то, что ожидают: аккуратный
405с заголовкомAllowпридётся собирать вручную.
Для CRUD-сервиса на десяток эндпоинтов это терпимо. Для API с версионированием, вложенными группами и общим middleware ручная обвязка быстро становится больше самой логики.
Три реальных варианта
net/http (1.22+)
Ноль зависимостей, ноль сюрпризов при обновлении Go, знакомый всем интерфейс http.Handler. Middleware пишется как функция, оборачивающая http.Handler — это стандартный паттерн, но собирать цепочку вы будете руками.
chi
go-chi/chi (актуальна ветка v5) построен ровно поверх http.Handler и http.HandlerFunc. Ключевое отличие от stdlib — группы, Route/Mount и совместимая с экосистемой цепочка middleware через Use. Хендлеры остаются обычными http.HandlerFunc, поэтому переход с stdlib почти механический, а любой middleware, написанный под http.Handler, работает без адаптеров.
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger) // общий middleware
r.Use(middleware.Recoverer)
r.Route("/api/v1", func(r chi.Router) {
r.Get("/users/{id}", getUser) // группа с общим префиксом
})
http.ListenAndServe(":8080", r)
}
func getUser(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id") // извлечение параметра
w.Write([]byte("user " + id))
}
Точные имена пакетов middleware сверяйте в документации chi: набор менялся между версиями, и не все, что были в v4, перешли в v5 под теми же именами.
gin
gin-gonic/gin — не просто роутер, а мини-фреймворк. У него свой *gin.Context вместо пары ResponseWriter/Request, встроенный биндинг JSON в структуры, валидация тегами, рендеринг. Радиксное дерево маршрутов исторически быстрое. Плата — вы уходите от стандартного интерфейса: middleware и хендлеры под http.Handler напрямую не вставишь, а миграция на другой роутер потом означает переписывание сигнатур.
Выбор роутера в Go — это выбор, остаётесь ли вы на интерфейсе http.Handler. Пока остаётесь, смена библиотеки стоит часы. Как только берёте свой Context, она стоит недели.
Сравнение
| Критерий | net/http 1.22+ | chi v5 | gin |
|---|---|---|---|
| Внешние зависимости | нет | одна лёгкая | тянет пачку |
| Совместимость с http.Handler | полная | полная | через адаптер |
| Группы и вложенные префиксы | нет | да | да |
| Цепочка middleware | вручную | встроенная | встроенная |
| Биндинг и валидация | руками | руками | из коробки |
| Порог входа | минимальный | низкий | средний |
Когда что выбирать
- net/http — сервис до полутора-двух десятков эндпоинтов, без сложного версионирования, где важна минимизация зависимостей (например, библиотека, которую подключают другие). Один-два middleware вы обернёте руками без страданий.
- chi — REST API среднего размера с группами, версиями и общей цепочкой middleware, где вы хотите остаться на
http.Handlerи сохранить свободу смены роутера. Практический дефолт для большинства HTTP-сервисов. - gin — когда команде нужен готовый пакет с биндингом, валидацией и рендерингом, и вы сознательно принимаете привязку к его Context. Оправдан, если проект целиком строится вокруг этого фреймворка, а не собирается из независимых кусков.
Отдельно про производительность: на реальном HTTP-сервисе стоимость самого матчинга маршрута обычно тонет в работе хендлера, БД и сериализации. Разница между роутерами в бенчмарках на голом матчинге редко проявляется в проде. Замеряйте на своём сценарии под своей нагрузкой, а не по чужим графикам «миллион маршрутов в секунду».
Что ломается на практике
- Приоритет маршрутов. Пересекающиеся паттерны (
/users/{id}и/users/me) в разных роутерах разрешаются по-разному. В stdlib 1.22 правила описаны, но проверьте руками, что/users/meне уходит в хендлер сid=me. - Trailing slash.
/usersи/users/— дляServeMuxэто разные вещи, и он может отдать редирект. У chi поведение настраивается middleware. Определитесь заранее, иначе получите неожиданные301. - Method Not Allowed vs Not Found. Клиент, дёрнувший
POSTтуда, где есть толькоGET, должен получить405сAllow, а не404. На stdlib это собирается вручную. - Смешивание стилей. Половина хендлеров на
gin.Context, половина наhttp.Handlerчерез адаптер — источник багов с записью тела ответа дважды. Выбирайте один стиль на сервис.
Что делать дальше
Проверьте версию Go в проекте: go version. Если ниже 1.22, обновлённый ServeMux вам недоступен и сравнение начинается с внешних роутеров. Если сервис уже на chi или gin — не мигрируйте на stdlib ради моды: выигрыш в зависимостях не окупит переписывание работающего кода. Для нового сервиса начните с net/http и добавьте chi в тот момент, когда ручная обвязка middleware и префиксов станет заметной. Настройте единый обработчик 405/404 и зафиксируйте политику trailing slash до того, как маршрутов станет много.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: net/http — документация стандартной библиотеки Go, go-chi/chi — репозиторий на GitHub
Частые вопросы
У меня Go 1.21, path-параметры в ServeMux не работают. Что делать?
Маршрут /users/me уходит в хендлер с id=me. Как исправить?
chi заметно медленнее stdlib или gin?
Можно ли использовать middleware, написанный под http.Handler, вместе с gin?
Стоит ли мигрировать работающий сервис с gin на stdlib из-за обновления 1.22?
Как правильно отдавать 405 Method Not Allowed на чистом net/http?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.