Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 24 из 25 ~16 мин +50 XP

Chi против Gin, Echo и стандартного mux

Сравнение роутеров go редко доходит до критериев: в чате звучат «gin быстрее», «mux теперь в stdlib» и «chi — это чистый net/http», и все правы по-своему. Возьмём пять осей — совместимость с net/http, зависимости, параметры, middleware, биндинги — и пройдём по ним chi, gin, echo и ServeMux из Go 1.22, с честными границами каждого и ценой переезда.

Содержание урока · 10 разделов

В пулреквесте — перенос трёх хендлеров из старого 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

GO
func 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: маршруты без зависимостей

GO
mux := 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?

Критерий → chi / gin / echo / mux#

Критерийchiginechomux 1.22+
Совместимость с net/http100%: http.HandlerFuncсвой *gin.Contextсвой echo.Context100%, stdlib
Зависимоститолько stdlibсвоё дерево пакетовсвоё дерево пакетовstdlib
URL-параметрыchi.URLParamc.Paramc.Paramr.PathValue
Middlewarefunc(http.Handler) http.Handler + готовыеfunc(*gin.Context)собственные echo-типынет встроенных
Биндинги и валидацияруками через encoding/jsonBindJSON + 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 пишется один раз.

Чек-лист выбора роутера#

  • Первый вопрос — останется ли 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.

// было на gin: func getNote(c *gin.Context) { id := c.("id") } // стало на chi: func getNote(w http., r *http.Request) { id := chi.(r, "id") }

Соберите минимальный сервис на chi

Расставьте строки: от создания роутера до ListenAndServe — middleware, группа, маршрут.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: роутеры Go

Термин

chi

нажмите, чтобы перевернуть
Определение

Роутер на чистом net/http: хендлеры http.HandlerFunc, middleware func(http.Handler) http.Handler, ноль зависимостей сверх stdlib. URLParam, Route/Group/Mount, пакет middleware с готовыми слоями.

1 / 6

Критерии сведены, границы обозначены: 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 — три версии покажут цену каждого выбора лучше любой таблицы.

Личные заметкиПусто
Хранятся только в вашем браузере (Chi · Chi против Gin, Echo и стандартного mux)

Вопросы и ответы по уроку

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. Постоянная жизнь двух роутеров в одном сервисе — техдолг, а не стратегия: два стека слоёв путают поведение.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Redis25 уроков~12 чпрактика25 уроков уже доступны

Redis в Go: кэш, сессии и очереди на go-redis

Курс Redis для Go-разработчиков: кэширование запросов, сессии, счётчики, Pub/Sub, Streams и rate limiter. 25 уроков с примерами кода go-redis v9.

  • Подключаться к Redis из Go и настраивать пул соединений
  • Кэшировать результаты запросов и придумывать TTL
  • Работать с хэшами, списками, множествами и ZSET
  • Строить сессии, счётчики, лидерборды и rate limiter