---
title: "Chi против Gin, Echo и стандартного mux"
description: "Chi vs gin, echo и ServeMux 1.22: сравнение роутеров go по совместимости с net/http, c *gin.Context, параметрам, middleware и биндингам — с границами."
course: "Chi: роутер для Go HTTP-сервисов"
url: "https://golangmaster.ru/courses/chi/lessons/24-chi-vs-drugie/"
published: "2026-09-19"
updated: "2026-09-27"
minutes: 16
---

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

*Курс «Chi: роутер для Go HTTP-сервисов», урок 24 из 25. ~16 минут. Обновлено 27.09.2026. Источник: https://golangmaster.ru/courses/chi/lessons/24-chi-vs-drugie/*

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

В пулреквесте — перенос трёх хендлеров из старого 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](/courses/chi/lessons/05-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. Что придётся переписать в каждом хендлере?

- ✅ Сигнатуру func(c *gin.Context) → func(w http.ResponseWriter, r *http.Request) и все обращения к c.Param и c.JSON
- Только имена функций: логика переносится заменой импорта
- Ничего: chi совместим с gin.Context через встроенный адаптер
- Только middleware: хендлеры gin и chi идентичны

gin.Context — конкретный тип фреймворка, а не обёртка над стандартными w и r: каждая ссылка на c.Param, c.JSON и c.Query при переезде превращается в chi.URLParam, энкодер и r.URL.Query. Совместимость chi с net/http тут не помогает: gin-сигнатуры не про net/http. Переезд тем дешевле, чем меньше хендлеров успело накопиться.

### 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-запросе?

```go
// один и тот же запрос 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"))
```

**Что выведет программа?**

- ✅ Пустую строку: RouteContext в контексте нет — его кладёт только роутер chi
- 42: URLParam читает паттерн из запроса любого роутера
- Панику: URLParam не находит RouteContext и падает
- id: вернётся имя параметра

URLParam читает RouteContext, который chi кладёт в контекст при совпадении своего маршрута — ServeMux про него не знает и ничего не кладёт, поэтому chi честно возвращает пустую строку. Для mux-варианта нужен r.PathValue("id") — его заполняет ServeMux. Два параметр-механизма не пересекаются, и тест через httptest ловит такую путаницу сразу.

### 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](/courses/chi/lessons/04-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?

- ✅ Встроенных middleware-цепочек: слои навешиваются обёртками руками
- Методов в паттернах: mux понимает только путь
- Wildcards в фигурных скобках
- r.PathValue для параметров пути

Методы в паттернах, {id} и PathValue появились ровно в 1.22 — а middleware-цепочек у mux нет ни в какой версии: он маршрутизирует, но не оборачивает хендлеры слоями. Для логов, восстановления после паник и авторизации придётся писать обёртки вида func(next http.Handler) http.Handler вручную — это сигнатура chi, только без готовых утилит.

> **Проект на gin вырос — 40 хендлеров с c *gin.Context.** Техлид формулирует «переходим на 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.

```go
// было на gin:
func getNote(c *gin.Context) {
    id := c.___("id")
}

// стало на chi:
func getNote(w http.___, r *http.Request) {
    id := chi.___(r, "id")
}
```

Подсказка: В gin параметр достаёт c.Param; в chi сигнатура — стандартные w http.ResponseWriter, а параметр читает chi.URLParam(r, "id").

Правильные ответы в пропусках: `Param`, `ResponseWriter`, `URLParam`

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

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

Правильный порядок строк:

1. `r := chi.NewRouter()`
2. `r.Use(middleware.RequestID)`
3. `r.Route("/notes", func(r chi.Router) {`
4. `    r.Get("/{id}", getNote)`
5. `})`
6. `http.ListenAndServe(":8080", r)`

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

- **chi** — Роутер на чистом net/http: хендлеры http.HandlerFunc, middleware func(http.Handler) http.Handler, ноль зависимостей сверх stdlib. URLParam, Route/Group/Mount, пакет middleware с готовыми слоями.
- ***gin.Context** — Контекст-швейцарский нож gin: параметры, query, биндинг, ответ и abort в одном типе. Хендлеры func(c *gin.Context) связывают код с фреймворком — переезд на net/http переписывает все сигнатуры.
- **echo.Context** — Собственный интерфейс echo: c.Param, c.Bind, c.JSON. Отличие от gin — хендлер возвращает error, который в HTTP-ответ превращает центральный обработчик фреймворка.
- **ServeMux Go 1.22+** — Методы в паттернах ("GET /notes/{id}"), wildcards {id} и {path...}, значения через r.PathValue. Нет middleware-цепочек, групп и готовых утилит вроде Timeout и Recoverer.
- **Критерий совместимости** — chi и mux работают на http.Handler — httptest и чужие middleware без адаптеров; gin и echo приносят свои контексты, и код связывается с их типами.
- **Цена переезда** — Сигнатуры всех хендлеров, биндинг-слой, middleware: c.JSON → заголовок плюс энкодер, c.Param → URLParam или PathValue. Чем позже переезд, тем дороже — считайте до старта проекта.

Критерии сведены, границы обозначены: chi для чистоты net/http и переезда по кускам, gin для биндингов из коробки, echo — если ближе error-стиль, mux 1.22+ — если middleware не нужны. Сравнение роутеров заканчивается не бенчмарком, а планом: посчитайте хендлеры, озвучьте команде цену связанности и зафиксируйте решение в README, пока спор не повторился на следующем сервисе. Разобрать gin и echo глубже можно на [курсе gin](/courses/gin/) и [курсе echo](/courses/echo/) — там те же задачи решаются их средствами, и разница перестаёт быть абстрактной.

## Коротко: что запомнить

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