Содержание урока · 10 разделов
- Сравнение роутеров go: критерии, а не бенчмарки
- Chi и gin: c *gin.Context против http.HandlerFunc
- Echo: свой контекст и error в возвращаемом значении
- ServeMux Go 1.22+: методы в паттернах и wildcards
- Критерий → chi / gin / echo / mux
- Когда chi, когда gin: границы выбора
- Чек-лист выбора роутера
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
В пулреквесте — перенос трёх хендлеров из старого gin-сервиса в новый проект на chi, и диф растёт на глазах: каждый func(c *gin.Context) переписывается в http.HandlerFunc, каждый c.Param("id") — в chi.URLParam(r, "id"), каждый c.JSON(...) — в два вызова: заголовок и энкодер. Три хендлера — сто строк дифа, а теперь представьте сорок. Сравнение роутеров go — это не спор о бенчмарках, а вопрос о том, чем вы связываете HTTP-слой: типами фреймворка или стандартной библиотеки. Пройдём по критериям — совместимость с net/http, вес, параметры, middleware, готовые биндинги — и по границам, где chi, gin, echo и ServeMux из Go 1.22 действительно лучше остальных.
Сравнение роутеров go: критерии, а не бенчмарки#
Скорость — худший критерий из всех: у chi, gin и echo под капотом radix-дерево, разрывы между ними живут в единицах микросекунд и исчезают за первым же запросом к базе. Критерии, которые решают на годы. Совместимость с net/http: chi и ServeMux работают на http.HandlerFunc и http.ResponseWriter — любой код стандартной библиотеки, чужие middleware и httptest подходят без адаптеров; gin и echo приносят свои типы контекста. Вес зависимостей: chi не тянет ничего сверх stdlib, gin и echo — самодостаточные фреймворки со своим деревом пакетов. URL-параметры есть у всех, но API разные: chi.URLParam против c.Param против r.PathValue. Middleware: у chi — func(http.Handler) http.Handler, у gin и echo — собственные сигнатуры, у mux — ничего. Биндинги и валидация: в gin и echo из коробки, в chi и mux — руками через encoding/json. По этим пяти осям выбор предсказуем.
Chi и gin: c *gin.Context против http.HandlerFunc#
gin ставит между хендлером и HTTP свой объект: func(c gin.Context) — в нём и запрос, и ответ, и параметры, и биндинг, и шорткаты для ошибок. Удобно: c.BindJSON декодирует и валидирует одним вызовом, c.JSON выставляет заголовок и пишет тело, c.AbortWithStatus рвёт цепочку. Цена — связность: сигнатура хендлера говорит о gin, тестам нужен gin-контекст, а переиспользовать хендлер вне gin нельзя. chi оставляет хендлер стандартным: func(w http.ResponseWriter, r http.Request) — параметры через chi.URLParam, ответ через энкодер, middleware — func(http.Handler) http.Handler. Связность наоборот: чуть больше ручной работы на каждый хендлер, зато HTTP-слой не знает о роутере вообще — те же функции принимает любой инструмент, совместимый с net/http.
Один хендлер на chi и на gin
GO// chi:
func getNote(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
note, err := store.Note(id)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(note)
}
// gin:
func getNote(c *gin.Context) {
note, err := store.Note(c.Param("id"))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, note)
}Один и тот же хендлер: в chi — стандартные w и r, ответ собирается заголовком и энкодером, ошибка — http.Error; в gin — контекст-швейцарский нож: c.Param читает параметр, c.JSON делает и заголовок, и тело, gin.H — алиас map[string]any. Версия chi работает с любым инструментом вокруг http.Handler, версия gin — только внутри gin. Оба варианта собираются локальным Go: chi без единой зависимости, gin — после go get github.com/gin-gonic/gin.
Проверка: цена переезда с gin
Проект перезжает с gin на chi. Что придётся переписать в каждом хендлере?
Echo: свой контекст и error в возвращаемом значении#
echo ближе к gin, чем к chi: хендлер получает собственный интерфейс echo.Context, параметры — c.Param, биндинг — c.Bind. Отличие в стиле: хендлер в echo возвращает error — func(echo.Context) error, — и центральный обработчик фреймворка превращает возвращённую ошибку в HTTP-ответ. Плюс: ошибки не теряются между слоями и обрабатываются в одном месте — идея та же, что у errorHandler из урока про централизованные ошибки chi, только там вы возвращаете их руками через w, а здесь язык протаскивает их за вас. Минус общий с gin: контекст — тип фреймворка, и переезд на net/http означает переписывание сигнатур. Выбор между gin и echo — вопрос вкуса и готовых деталей под задачу; по отношению к net/http они в одном лагере.
Тот же хендлер на echo
GOfunc getNote(c echo.Context) error {
note, err := store.Note(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, "not found")
}
return c.JSON(http.StatusOK, note)
}Сигнатура func(echo.Context) error — вся разница с gin в одном возвращаемом значении: echo.NewHTTPError несёт код и сообщение, их подхватит центральный обработчик фреймворка, и хендлеру не нужно самому писать в ответ. c.Param и c.JSON выглядят как в gin — тот же контекст-подход, свои типы. Собирается после go get github.com/labstack/echo/v4; в песочнице, как и весь код библиотечных курсов, не исполняется — нужен компилятор.
Что напечатает chi.URLParam на mux-запросе?
Прочитайте код и выберите, что он выведет:
// один и тот же запрос GET /notes/42
// вариант A — chi:
r.Get("/notes/{id}", getNote) // chi.URLParam(r, "id")
// вариант B — ServeMux 1.22:
mux.HandleFunc("GET /notes/{id}", getNote) // r.PathValue("id")
// хендлер A случайно позвали на запросе варианта B:
fmt.Print(chi.URLParam(r, "id"))ServeMux Go 1.22+: методы в паттернах и wildcards#
Долгое время главный аргумент против mux — скудные паттерны: точные пути и префиксы, и всё. Go 1.22 закрыл дыру: метод в паттерне — mux.HandleFunc("GET /notes/{id}", h), wildcard в фигурных скобках на один сегмент, {path...} — на хвост пути, {$} — точное совпадение корня. Значения достаются r.PathValue("id"). Для маленького сервиса с пятью-десятью ручками этого хватает, и зависимостей ноль. Чего нет — всего, чем chi отличался: middleware-цепочек (каждый слой вешается обёрткой руками), групп и подроутеров, готовых утилит — RequestID, Timeout и Recoverer придётся писать или тащить из чужих пакетов. Параметры из урока про URLParam у chi и PathValue у mux решают одну задачу разными API — но экосистема вокруг остаётся за chi.
ServeMux 1.22: маршруты без зависимостей
GOmux := http.NewServeMux()
mux.HandleFunc("GET /notes/{id}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(note(r.PathValue("id")))
})
mux.HandleFunc("POST /notes", createNote)
log.Fatal(http.ListenAndServe(":8080", mux))Паттерн "GET /notes/{id}" читается как объявление: метод, путь, параметр. PathValue достаёт id, заполненный mux-ом при совпадении. Разные методы одного пути — разные паттерны, конфликт mux разрулит сам: чужой метод получит 405. Код запускается локальным Go 1.22+ без единой зависимости — go run main.go и curl /notes/42. Как только понадобились middleware-цепочки или группы — это уже история про chi.
Проверка: границы ServeMux
Какого механизма нет в ServeMux из Go 1.22?
Техлид формулирует «переходим на net/http», и план превращается в переписывание всех сигнатур: сорок хендлеров func(c *gin.Context), каждый c.JSON — в заголовок плюс энкодер, слой биндингов — поверх encoding/json с собственной валидацией, middleware — в func(http.Handler) http.Handler. Гитхаб-диф на тысячи строк, тесты переписываются следом. Я не против gin — я против связывания HTTP-слоя с фреймворком без осознания цены: на старте проекта она ноль, к сороковым хендлерам — недели работы. Если переезд неизбежен, его режут по кускам: chi монтирует любой http.Handler, и gin-часть живёт рядом, пока не переварится.
Критерий → chi / gin / echo / mux#
| Критерий | chi | gin | echo | mux 1.22+ |
|---|---|---|---|---|
| Совместимость с net/http | 100%: http.HandlerFunc | свой *gin.Context | свой echo.Context | 100%, stdlib |
| Зависимости | только stdlib | своё дерево пакетов | своё дерево пакетов | stdlib |
| URL-параметры | chi.URLParam | c.Param | c.Param | r.PathValue |
| Middleware | func(http.Handler) http.Handler + готовые | func(*gin.Context) | собственные echo-типы | нет встроенных |
| Биндинги и валидация | руками через encoding/json | BindJSON + validator из коробки | встроенный биндер | руками |
| Группы и подроутеры | Route, Group, Mount | группы есть | группы есть | нет |
Когда chi, когда gin: границы выбора#
chi — когда HTTP-слой должен остаться на net/http: миграция старого кода по кускам, переиспользование middleware из экосистемы chi, тесты httptest без адаптеров, долгоживущий сервис, где связанность с фреймворком дорого обойдётся. gin — когда нужны биндинги и валидация прямо сейчас, а команда не хочет собирать этот слой сама: binder, валидатор и контекст работают из коробки, и для типового CRUD это честная экономия дней. echo — тот же лагерь, что gin, с error в возврате хендлера: берите его, если стиль «хендлер возвращает ошибку» ближе команде. ServeMux 1.22+ — когда middleware не нужны и зависимостей быть не должно: крошечный внутренний сервис, утилита, пара health-эндпойнтов. Моё спорное: для нового REST-сервиса с планами на годы я беру chi — стандартные типы переживут смену модных роутеров, а биндинг-слой на encoding/json пишется один раз.
Честная граница каждого. chi: берите, если цените чистый net/http и экосистему middleware — и готовы руками написать биндинг-слой. gin: берите, если хочется batteries included, а задача — типовой CRUD с валидацией; цена — сигнатуры фреймворка надолго. echo: между gin и chi, error-стиль приятен, если ошибки у вас и так протаскиваются наверх. mux: только когда middleware действительно не нужны — иначе обёртки руками размножатся по всему коду. Неверный выбор не фатален: фатален неосознанный — когда команда берёт фреймворк «за скорость», не зная, что сигнатуры контекста позже съедят всю экономию.
Чек-лист выбора роутера#
- Первый вопрос — останется ли HTTP-слой на типах net/http: chi и mux дают да, gin и echo — нет.
- Посчитайте хендлеры: чем их больше, тем дороже связанность с чужим контекстом при переезде.
- Биндинги и валидация: gin и echo дают из коробки, chi и mux — руками поверх encoding/json.
- Middleware: chi — сигнатура net/http и готовые утилиты; gin и echo — свои типы; mux — только руками.
- Проверьте тесты: httptest работает без адаптеров только с роутерами на http.Handler.
- Миграция: chi монтирует любые http.Handler, что позволяет переезжать по кускам, а не big bang.
Переведите хендлер с gin на chi
Три пропуска: чтение параметра в gin, сигнатура chi и чтение параметра в chi.
Соберите минимальный сервис на chi
Расставьте строки: от создания роутера до ListenAndServe — middleware, группа, маршрут.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: роутеры Go
Критерии сведены, границы обозначены: chi для чистоты net/http и переезда по кускам, gin для биндингов из коробки, echo — если ближе error-стиль, mux 1.22+ — если middleware не нужны. Сравнение роутеров заканчивается не бенчмарком, а планом: посчитайте хендлеры, озвучьте команде цену связанности и зафиксируйте решение в README, пока спор не повторился на следующем сервисе. Разобрать gin и echo глубже можно на курсе gin и курсе echo — там те же задачи решаются их средствами, и разница перестаёт быть абстрактной.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Критерий первый — совместимость с net/http: chi и mux живут на stdlib-типах, gin и echo приносят свои контексты.
- gin.Context собирает параметры, биндинги и ответ в одном объекте — удобно сейчас, дорого при переезде.
- echo возвращает error из хендлера — централизованная обработка ошибок встроена во фреймворк.
- ServeMux Go 1.22+ умеет методы в паттернах и wildcards {id}, но не имеет middleware-цепочек и групп.
- Скорость у всех с запасом: radix-деревья, разница видна только в микробенчмарках на пустых маршрутах.
- Переезд с gin на net/http — это переписывание сигнатур всех хендлеров, биндинг-слоя и middleware.
- chi — за чистый net/http и экосистему middleware, gin — за биндинги из коробки, mux — за ноль зависимостей.
Домашнее задание
Возьмите хендлер getNote с параметром id и JSON-ответом и реализуйте его трижды: на chi с chi.URLParam, на ServeMux 1.22 с r.PathValue и на gin с c.Param. Сравните сигнатуры, тесты через httptest и количество зависимостей в go.mod — три версии покажут цену каждого выбора лучше любой таблицы.
Личные заметкиПусто
Вопросы и ответы по уроку
Gin быстрее chi — стоит ли ради этого менять роутер?
На микробенчмарках разрывы есть, в реальном сервисе их съедают база, сеть и сериализация: radix-дерево у всех современных роутеров. Роутер выбирают по совместимости, middleware и экосистеме, а не по наносекундам на пустом маршруте. Если сервис уже работает — переработка HTTP-слоя ради скорости роутера почти никогда не окупается.
ServeMux после Go 1.22 делает chi ненужным?
Нет: mux закрыл маршрутизацию — методы в паттернах, wildcards, PathValue. Но у него нет middleware-цепочек, групп, подроутеров и готовых утилит вроде RequestID, Timeout и Recoverer. Для крошечного сервиса этого хватает; как только слои и группировка становятся нужны, вы либо пишете их руками, либо это ровно chi.
Можно ли смешивать chi и gin в одном сервисе?
Да, на границе http.Handler: chi монтирует любой http.Handler методом Mount, и gin-роутер может жить внутри как отдельный кусок — так переезжают по частям. Постепенно gin-часть сокращается, а общие middleware уже на chi. Постоянная жизнь двух роутеров в одном сервисе — техдолг, а не стратегия: два стека слоёв путают поведение.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Redis в Go: кэш, сессии и очереди на go-redis
Курс Redis для Go-разработчиков: кэширование запросов, сессии, счётчики, Pub/Sub, Streams и rate limiter. 25 уроков с примерами кода go-redis v9.
- Подключаться к Redis из Go и настраивать пул соединений
- Кэшировать результаты запросов и придумывать TTL
- Работать с хэшами, списками, множествами и ZSET
- Строить сессии, счётчики, лидерборды и rate limiter