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