Много лет я использовал сторонние пакеты, чтобы удобнее структурировать и управлять middleware в Go-веб-приложениях. В небольших проектах я часто брал alice, чтобы собирать «цепочки» middleware, которые можно переиспользовать на разных маршрутах. А в более крупных приложениях, где много middleware и маршрутов, я обычно использовал роутер вроде chi или flow, чтобы делать вложенные «группы» маршрутов со своим набором middleware для каждой группы.
Но после того как в Go 1.22 в http.ServeMux появилась новая функциональность сопоставления по шаблонам (pattern matching), я по возможности стал убирать сторонние зависимости из логики маршрутизации и переходить на одну лишь стандартную библиотеку.
Однако полный переход на стандартную библиотеку оставляет хороший вопрос: как организовать и управлять middleware без использования сторонних пакетов?
Почему управление middleware — это проблема?
Если в приложении всего несколько маршрутов и middleware-функций, проще всего оборачивать ваши обработчики в нужные middleware для каждого маршрута отдельно. Примерно так:
// На этом маршруте middleware нет. mux.Handle("GET /static/", http.FileServerFS(ui.Files)) // Оба этих маршрута используют middleware requestID и logRequest. mux.Handle("GET /", requestID(logRequest(http.HandlerFunc(home)))) mux.Handle("GET /article/{id}", requestID(logRequest(http.HandlerFunc(showArticle)))) // На этом маршруте дополнительно используются middleware authenticateUser и requireAdminUser. mux.Handle("GET /admin", requestID(logRequest(authenticateUser(requireAdminUser(http.HandlerFunc(showAdminDashboard))))))
Это работает и не требует внешних зависимостей, но вы, вероятно, представляете, какие минусы появятся по мере роста числа маршрутов:
Есть повторения в объявлениях маршрутов.
Становится сложнее читать код и быстро понимать, какие маршруты используют один и тот же набор middleware.
Это выглядит довольно «хрупко»: в боль��ом приложении, если нужно добавить, убрать или поменять местами middleware на множестве маршрутов, легко пропустить один из них и не заметить ошибку.
Альтернатива alice
Как я вкратце упоминал выше, пакет alice позволяет объявлять и переиспользовать «цепочки» middleware. Мы могли бы переписать пример выше, используя alice, вот так:
mux := http.NewServeMux() // Создаём базовую цепочку middleware. baseChain := alice.New(requestID, logRequest) // Расширяем базовую цепочку middleware аутентификации для маршрутов только для админов. adminChain := baseChain.Append(authenticateUser, requireAdminUser) // На этом маршруте middleware нет. mux.Handle("GET /static/", http.FileServerFS(ui.Files)) // Публичные маршруты используют базовые middleware. mux.Handle("GET /", baseChain.ThenFunc(home)) mux.Handle("GET /article/{id}", baseChain.ThenFunc(showArticle)) // Админские маршруты с дополнительными middleware аутентификации. mux.Handle("GET /admin", adminChain.ThenFunc(showAdminDashboard))
На мой взгляд, этот код заметно чище, и в значительной степени снимает три проблемы, о которых мы говорили выше.
Но если вам не хочется добавлять alice в зависимости, можно воспользоваться функцией slices.Backward, появившейся в Go 1.23, и буквально в несколько строк написать собственный тип chain:
type chain []func(http.Handler) http.Handler func (c chain) thenFunc(h http.HandlerFunc) http.Handler { return c.then(h) } func (c chain) then(h http.Handler) http.Handler { for _, mw := range slices.Backward(c) { h = mw(h) } return h }
После этого тип chain можно использовать при объявлении маршрутов вот так:
mux := http.NewServeMux() // Создаём базовую цепочку middleware. baseChain := chain{requestID, logRequest} // Расширяем базовую цепочку middleware аутентификации для маршрутов только для админов. adminChain := append(baseChain, authenticateUser, requireAdminUser) mux.Handle("GET /static/", http.FileServerFS(ui.Files)) mux.Handle("GET /", baseChain.thenFunc(home)) mux.Handle("GET /article/{id}", baseChain.thenFunc(showArticle)) mux.Handle("GET /admin", adminChain.thenFunc(showAdminDashboard))
Синтаксис здесь не в точности такой же, как в alice, но очень близкий, а по поведению это, по сути, то же самое.
Если вам интересно применить этот подход в своём коде, я выложил тесты для типа chain в этом gist.
Альтернатива chi и похожим роутерам
В крупных приложениях, когда у меня есть много разных middleware, которые используются на множестве разных маршрутов, функциональность группировки маршрутов, которую дают роутеры вроде chi и flow, всегда очень выручала.
По сути, они позволяют создавать группы маршрутов с определённым набором middleware. Причём эти группы можно вкладывать друг в друга: дочерние группы «наследуют» middleware родительских групп и могут дополнять их своим набором.
Давайте посмотрим на пример с использованием chi — насколько я помню, это был первый роутер, который поддержал такой стиль группировки маршрутов.
r := chi.NewRouter() r.Use(recoverPanic) // «Глобальный» middleware, используется на всех маршрутах. r.Method("GET", "/static/", http.FileServerFS(ui.Files)) // Создаём группу маршрутов. r.Group(func(r chi.Router) { // Добавляем middleware для группы. r.Use(requestID) r.Use(logRequest) // Маршруты, объявленные внутри группы, будут использовать этот набор middleware. r.Get("/", home) r.Get("/article/{id}", showArticle) // Создаём вложенную группу маршрутов. Любые маршруты в этой группе будут // использовать middleware, объявленные в самой группе, и в родительских группах. r.Group(func(r chi.Router) { r.Use(authenticateUser) r.Use(requireAdminUser) r.Get("/admin", showAdminDashboard) }) })
Но если вы хотите остаться в рамках стандартной библиотеки, то сделать собственную реализацию роутера, которая оборачивает http.ServeMux и поддерживает группы middleware в похожем стиле, не так уж сложно:
type Router struct { globalChain []func(http.Handler) http.Handler routeChain []func(http.Handler) http.Handler isSubRouter bool *http.ServeMux } func NewRouter() *Router { return &Router{ServeMux: http.NewServeMux()} } func (r *Router) Use(mw ...func(http.Handler) http.Handler) { if r.isSubRouter { r.routeChain = append(r.routeChain, mw...) } else { r.globalChain = append(r.globalChain, mw...) } } func (r *Router) Group(fn func(r *Router)) { subRouter := &Router{routeChain: slices.Clone(r.routeChain), isSubRouter: true, ServeMux: r.ServeMux} fn(subRouter) } func (r *Router) HandleFunc(pattern string, h http.HandlerFunc) { r.Handle(pattern, h) } func (r *Router) Handle(pattern string, h http.Handler) { for _, mw := range slices.Backward(r.routeChain) { h = mw(h) } r.ServeMux.Handle(pattern, h) } func (r *Router) ServeHTTP(w http.ResponseWriter, rq *http.Request) { var h http.Handler = r.ServeMux for _, mw := range slices.Backward(r.globalChain) { h = mw(h) } h.ServeHTTP(w, rq) }
А затем вы можете использовать тип Router в своём коде вот так:
r := NewRouter() r.Use(recoverPanic) r.Handle("GET /static/", http.FileServerFS(ui.Files)) r.Group(func(r *Router) { r.Use(requestID) r.Use(logRequest) r.HandleFunc("GET /", home) r.HandleFunc("GET /article/{id}", showArticle) r.Group(func(r *Router) { r.Use(authenticateUser) r.Use(requireAdminUser) r.HandleFunc("GET /admin", showAdminDashboard) }) })
Для тех, кто хочет системно и грамотно изучить Go с нуля, обратите внимание на курс Golang Developer. Basic от OTUS. Там много практики: инструменты языка, Git и Docker, конкурентность с горутинами и каналами, API (OpenAPI/Swagger) и работа с хранилищами и брокерами — ровно то, что потом приходится поддерживать в проде.
Чтобы узнать больше о формате обучения и познакомиться с преподавателями, приходите на бесплатные демо-уроки:
3 февраля, 20:00. «Примитивы синхронизации в Go». Записаться
11 февраля, 20:00. «Бот-сторож на Golang. Асинхронная верификация без паролей». Записаться
19 февраля, 20:00. «Многопоточность в Golang с нуля». Записаться
