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

В большинстве языков берут фреймворк — и он сам решает, как принять запрос, разобрать 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 устроен просто: он умеет сопоставить путь /products/ с функцией, и всё. Он не умеет:

  • вытащить id из пути /products/42 — придётся парсить строку вручную;
  • применить одно middleware ко всей группе маршрутов;
  • построить дерево маршрутов с общим префиксом /api/v1/....

На небольшом проекте хватает. На реальном сервисе — быстро превращается в ручной разбор строк и strings.HasPrefix. Поэтому берут 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} — это параметр пути.

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())

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

Middleware

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

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

r := chi.NewRouter()
r.Use(middleware.RequestID)   // добавить X-Request-Id в каждый запрос
r.Use(middleware.Recoverer)   // поймать панику и вернуть 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, err.Error(), http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(result)
}

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

Коротко

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

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

  • Обработчики и JSON в Go — декодирование тела запроса и сериализация ответа.
  • Структура проекта и проводка зависимостей — как собрать роутер и подключить обработчики в main.
  • Middleware в Go — написать собственный middleware и цепочки из них.