← назад к разделу

GO КАК ФУНДАМЕНТ Роутинг

Роутинг в Go: net/http и chi

Go

Стандартный Handler, chi, параметры URL, группы маршрутов и middleware без лишней обвязки.

1. Handler - обычный интерфейс

1 / 5

HTTP-обработчик принимает ResponseWriter и *http.Request. Совместимость с net/http позволяет менять роутер без изменения логики.

  • HandleFunc регистрирует функцию.
  • ResponseWriter отвечает за статус, заголовки и тело.
  • Request содержит метод, путь, параметры и контекст.
*http.Requesthandlerresponse

2. Chi добавляет удобную маршрутизацию

2 / 5

Chi тонко расширяет стандартный Handler: методы, параметры, группы и middleware остаются обычным Go-кодом.

  • r.Get, r.Post регистрируют метод и путь.
  • chi.URLParam читает переменную маршрута.
  • Совместимые middleware можно переносить.
chi.RouterGET /users/{id}POST /usershandler

3. Параметры приходят из URL

3 / 5

Шаблон /users/{id} выделяет динамическую часть. Её проверяют и преобразуют перед вызовом хранилища.

  • Параметр - строка, число требует Parse.
  • Некорректный id возвращает 400, отсутствие записи - 404.
  • Не смешивайте параметры пути и query без причины.
/users/42id = 42store

4. Группы задают общие правила

4 / 5

Роуты группируются по префиксу и общей обвязке. Например, публичные маршруты, API и админка получают разные middleware.

  • r.Route создаёт подроутер.
  • r.Use подключает middleware группы.
  • Общие правила не копируются в каждый handler.
router/public/api + authhandlers

5. Middleware идёт до и после handler

5 / 5

Обвязка выполняет сквозные задачи: логирование, request id, auth, восстановление panic. Затем вызывается следующий обработчик.

  • Middleware принимает и возвращает Handler.
  • Порядок цепочки важен.
  • Сквозная логика не дублируется внутри endpoint.
request idauthhandlerresponse

← назад к разделу

В большинстве языков берут фреймворк — и он сам решает, как принять запрос, разобрать URL и вызвать нужный код. В Go всё это делается явно. Начнём с основ стандартной библиотеки и разберём, зачем добавляют chi.

Стандартный роутер: что умеет и чего не хватает

Go поставляется со встроенным веб-сервером. Его можно запустить буквально в несколько строк:

http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello world"))
})
http.ListenAndServe(":8080", nil)

Штатный роутер http.ServeMux долго умел только сопоставлять префикс пути с функцией, и id из /products/42 приходилось вырезать руками. С Go 1.22 он заметно вырос: шаблон маршрута принимает метод и параметры пути, а значение параметра отдаёт r.PathValue:

mux := http.NewServeMux()
mux.HandleFunc("GET /products/{id}", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "id=%s", r.PathValue("id"))
})

Для небольшого сервиса этого хватает, и брать библиотеку ради одних параметров больше не нужно. Чего у ServeMux по-прежнему нет: цепочек middleware и групп маршрутов с общим префиксом и общим middleware — их приходится собирать руками, оборачивая обработчики функциями. Поэтому на реальном сервисе всё ещё берут chi: он добавляет ровно это и ничего лишнего.

net/http: один интерфейс для всего

Прежде чем перейти к chi, важно понять одну вещь из net/http. Всё веб-программирование в Go строится на одном интерфейсе:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Любой объект, у которого есть метод ServeHTTP, можно передать в сервер. Обработчик — это Handler. Роутер — это тоже Handler. Middleware — это Handler, обёрнутый вокруг другого Handler. Именно это делает экосистему Go модульной: разные библиотеки используют один и тот же интерфейс и хорошо работают вместе.

Chi: тонкий роутер поверх net/http

Chi — это маршрутизатор, который добавляет удобство, не ломая совместимость. Роутер chi сам является http.Handler, его можно напрямую передать в http.ListenAndServe.

import "github.com/go-chi/chi/v5"

func newRouter(products *ProductHandler) http.Handler {
    r := chi.NewRouter()

    r.Get("/products", products.list)
    r.Post("/products", products.create)
    r.Get("/products/{id}", products.getOne)

    return r
}

r.Get, r.Post и другие методы регистрируют маршруты по HTTP-методу и пути. Фигурные скобки {id} — это параметр пути.

А где Gin, Echo и Fiber

Вопрос «какие фреймворки для Go знаете» на собеседовании почти обязателен, и ответ короткий. Gin и Echo это полноценные веб-каркасы со своим типом контекста и своей сигнатурой обработчика, у них быстрый роутер, привязка JSON и большой набор middleware, но обработчик для них это не http.Handler, и готовые middleware из экосистемы net/http приходится оборачивать. Fiber стоит особняком: он построен на fasthttp, а не на net/http, поэтому с http.Request и стандартными обработчиками несовместим вовсе; выигрыш в скорости заметен только там, где сервис отдаёт мелкие ответы миллионами. Chi остаётся на net/http: обработчики, middleware и тесты через httptest работают без переходников, и именно поэтому он здесь.

Что общего у всех: сервер net/http обслуживает каждое соединение в отдельной горутине, и обработчики выполняются одновременно. Любое общее состояние в обработчике, счётчик, кэш, map, должно быть защищено мьютексом или atomic, иначе гонка.

URL-параметры

Параметр из пути достаётся функцией chi.URLParam:

func (h *ProductHandler) getOne(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id") // строка "42" из пути /products/42
    // преобразование в число — явно:
    productID, err := strconv.Atoi(id)
    if err != nil {
        http.Error(w, "неверный id", http.StatusBadRequest)
        return
    }
    // ...
}

Go не приводит типы автоматически. id приходит строкой — и если нужно число, его конвертируют явно. Это небольшой ритуал, зато код прозрачен: видно, где может быть ошибка.

Группы маршрутов и подроутеры

Когда маршрутов становится много, их группируют по префиксу. Chi предлагает два способа.

r.Route — группа прямо в одном месте:

func newRouter(products *ProductHandler, orders *OrderHandler) http.Handler {
    r := chi.NewRouter()

    r.Route("/products", func(r chi.Router) {
        r.Get("/", products.list)
        r.Post("/", products.create)
        r.Get("/{id}", products.getOne)
        r.Delete("/{id}", products.delete)
    })

    r.Route("/orders", func(r chi.Router) {
        r.Get("/", orders.list)
        r.Post("/", orders.create)
    })

    return r
}

r.Mount — подключение роутера из другого пакета. Удобно, когда каждый домен собирает свои маршруты сам:

// в пакете orders:
func (h *OrderHandler) Routes() chi.Router {
    r := chi.NewRouter()
    r.Get("/", h.list)
    r.Post("/", h.create)
    return r
}

// в main:
r.Mount("/orders", orders.Routes())

Маршруты домена живут рядом с его кодом. Главный роутер только собирает части вместе.

Есть и третий способ — r.Group(func(r chi.Router) { ... }): группа без общего префикса, нужная только затем, чтобы повесить middleware на часть маршрутов — например, авторизацию на всё, кроме /health и /login.

Middleware

Middleware — это функция, которая выполняется до или после обработчика. Типичные задачи: логирование, восстановление после panic, установка заголовков, аутентификация.

В chi middleware навешивают через Use:

r := chi.NewRouter()
r.Use(middleware.RequestID)   // добавить X-Request-Id в каждый запрос
r.Use(middleware.Recoverer)   // поймать `panic` и вернуть 500 вместо краша

r.Route("/products", func(r chi.Router) {
    r.Use(authMiddleware)     // только для этой группы
    r.Get("/", products.list)
})

middleware.RequestID и middleware.Recoverer идут из самого chi — они готовы к использованию. Свой middleware пишут в том же виде: функция принимает http.Handler и возвращает http.Handler.

Как устроен обработчик

Обработчик в Go — это метод, который принимает http.ResponseWriter и *http.Request. Он должен быть простым: разобрал запрос, вызвал бизнес-логику, написал ответ.

func (h *ProductHandler) create(w http.ResponseWriter, r *http.Request) {
    var req CreateProductRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "неверный запрос", http.StatusBadRequest)
        return
    }
    result, err := h.createProduct(r.Context(), req)
    if err != nil {
        http.Error(w, "internal error", http.StatusInternalServerError) // текст внутренней ошибки клиенту не отдают
        return
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(result)
}

Обработчик знает про HTTP: коды ответа, заголовки, тело. Бизнес-правил в нём нет — их решает вызываемая функция h.createProduct. Такое разделение делает код проще для чтения и тестирования.

Коротко

  • Gin и Echo это каркасы со своим контекстом, Fiber живёт на fasthttp и несовместим с net/http; каждый запрос сервер обрабатывает в своей горутине, общее состояние обработчика нужно защищать.
  • net/http — стандартная библиотека для HTTP в Go; с Go 1.22 её ServeMux понимает метод и параметры пути (GET /products/{id}, r.PathValue), не хватает ему middleware и групп.
  • Всё вращается вокруг интерфейса http.Handler с одним методом ServeHTTP; роутеры и middleware реализуют тот же интерфейс.
  • Chi — тонкий роутер поверх net/http: добавляет параметры пути, группы и middleware, не создавая собственной экосистемы.
  • Параметр пути берут через chi.URLParam(r, "name"), он всегда строка — конвертировать явно.
  • r.Route группирует маршруты с общим префиксом, r.Mount подключает роутер из другого пакета, r.Group вешает middleware на часть маршрутов без префикса.
  • Middleware навешивают через r.Use; встроенные middleware.RequestID и middleware.Recoverer покрывают базовые нужды.
  • Обработчик отвечает только за HTTP-слой: разобрать запрос, вызвать логику, написать ответ.

Что пощупать

Маршруты каталога в практикуме remodov/marketplace-system-go: группа /products с командами на PATCH и POST, параметр пути через chi.URLParam, промежуточные обработчики RequestID, Logger и Recoverer.

Код: handler.go, main.go.

Что почитать дальше