В Go веб-слой строится на стандартной библиотеке net/http, а не на фреймворке. net/http даёт всё необходимое — интерфейс http.Handler, сервер, маршрутизацию, — но его штатный роутер скромен. Поэтому в UCP-стеке берут chi: тонкий роутер поверх net/http, полностью с ним совместимый.
net/http как основа
Всё в Go-вебе вращается вокруг одного интерфейса: http.Handler с методом ServeHTTP(w, r). Обработчики, middleware, роутеры — всё это http.Handler. Это и есть причина, по которой chi совместим со стандартной библиотекой: он не заменяет net/http, а надстраивается над ним.
Почему chi
Штатный http.ServeMux умеет базовую маршрутизацию, но не даёт удобных параметров пути, групп и middleware-цепочек из коробки. chi добавляет это, оставаясь «обычным» http.Handler — никакой своей экосистемы, никакого vendor lock-in. Роутер chi можно отдать в http.ListenAndServe как есть.
import "github.com/go-chi/chi/v5"
func newRouter(createProduct *CreateProductHandler) http.Handler {
r := chi.NewRouter()
r.Get("/products/{id}", createProduct.getOne)
r.Post("/products", createProduct.create)
return r
}
Параметры пути
Параметр объявляется в шаблоне фигурными скобками и достаётся через chi.URLParam.
func (h *ProductHandler) getOne(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
// ...
}
Преобразование id из строки в число и его проверку делают явно (Go не приводит типы за тебя) — это часть обработки запроса и валидации.
Группы и подроутеры
Маршруты одного домена группируют — общий префикс, общие middleware. chi даёт Route (вложенная группа) и Mount (подключение отдельного роутера).
func newRouter(products *ProductHandler, orders *OrderHandler) http.Handler {
r := chi.NewRouter()
r.Use(middleware.RequestID, middleware.Recoverer)
r.Route("/products", func(r chi.Router) {
r.Get("/", products.list)
r.Post("/", products.create)
r.Get("/{id}", products.getOne)
})
r.Mount("/orders", orders.Routes())
return r
}
Route собирает группу прямо здесь; Mount подключает роутер, собранный в пакете домена (orders.Routes() возвращает свой chi.Router). Это держит маршруты домена рядом с его кодом — та же раскладка по доменам, что и в структуре проекта. Middleware навешивается на роутер или группу через Use.
Тонкий обработчик UCP
Обработчик в UCP остаётся тонким: разобрать запрос, вызвать Handler сценария, написать ответ. Бизнес-логики в нём нет.
func (h *ProductHandler) create(w http.ResponseWriter, r *http.Request) {
var req CreateProductRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
product, err := h.createProduct.Handle(r.Context(), req.toCommand())
if err != nil {
writeError(w, mapError(err), err)
return
}
writeJSON(w, http.StatusCreated, productResponse(product))
}
Обработчик знает про HTTP, но не про правила: сценарий — в createProduct.Handle, собранном проводкой в main. Это та же граница «контроллер тонкий», что в Spring MVC, только без аннотаций. Дальше идут декодирование и кодирование JSON — и весь явный конвейер, который продукт-инженер держит под контролем, потому что видит его целиком.