Создание удобной обертки над Chi роутером с доступом к данным через context

В этой статье создадим обертку над Chi роутером, которая позволит получать данные через контекст как во всех современных фреймворках на языке Golang.

Оглавление статьи

  1. Вводная часть
  2. Подготовительные работы
  3. Создание роутера
  4. Создание контекста
  5. Создание роутера продолжение
  6. Использование сторонних middleware
  7. Подведем итоги

Вводная часть

В работе со стандартным роутером в Golang, обработчикам в качестве параметров передается запрос, ответ:

GOLANG
// http
func Handler(w http.ResponseWrite, r *http.Request) {
  // code
}

В работе с различными фреймворками на языке Golang, при создании обработчиков используется паттерн, где в качестве параметра передается context, например:

GOLANG
// Gin
func Handler(ctx *gin.Context) {
  // code
}

// Fiber
func Handler(ctx *fiber.Ctx) error {
  // code
}

Подход с контекстом является более гибким, он позволяет передавать помимо стандартного запроса, ответа еще и другие необходимые данные.

В этой статье мы создадим обертку, которая позволит создавать аналогичный с приведенными примерами обработчик.

GOLANG
// Chix
func Handler(ctx *chix.Ctx) error {
  // code
}

Подготовительные работы

Первым делом необходимо инициализировать наш пакет (не забудьте изменить ссылку на репозиторий и название пакета на свои):

BASH
go mod init github.com/eliofery/go-chix

Далее установим пакет роутера Chi на основе которого мы будем создавать свою обертку:

BASH
go get -u github.com/go-chi/chi/v5

После проделанных манипуляций создастся файл go.mod, примерно со следующим содержимым:

GO.MOD
module github.com/eliofery/go-chix

go 1.21.5

require github.com/go-chi/chi/v5 v5.0.12 // indirect

Создание роутера

Начнем с создания обертки над Chi роутером.

В корне проекта создадим файл router.go:

BASH
touch router.go

Внутри созданного файла создадим структуру нашего будущего роутера:

ROUTER.GO
package chix

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

// Router обертка над chi роутером
type Router struct {
  *chi.Mux
}

В структуре нашего роутера мы используем *chi.Mux которая является Chi роутером.

Далее создадим конструктор для нашей структуры Router:

ROUTER.GO
// NewRouter создание роутера
func NewRouter() *Router {
  return &Router{
    Mux: chi.NewRouter(),
  }
}

При создании нашего роутера создается новый роутер Chi, который мы будем использовать в описании своего обработчика.

В корне проекта создадим каталог _example и файл main.go внутри него:

BASH
mkdir _example
touch _example/main.go

Со следующим содержимым:

MAIN.GO
package main

import "github.com/eliofery/go-chix"

func main() {
  // Инициализация нашего роутера
  route := chix.NewRouter()
}

Теперь когда роутер, который является оберткой над роутером Chi создан, мы можем приступить к описанию методов.

Метод Get

Первый метод который мы опишем, будет метод Get. Рассмотрим подробнее, что происходит:

ROUTER.GO
// Get запрос на получение данных
// Обертка над методом Get у Chi роутера
// В качестве параметров мы так же используем path для определении маршрута,
// но далее мы переопределяем стандартный обработчик func(w http.ResponseWriter, r *http.Request) на handler Handler.
// Мы еще не описывали тип Handler, это будет сделано далее в статье.
func (rt *Router) Get(path string, handler Handler) {
  // Здесь мы вызываем стандартный метод Get у Chi роутера в обработчик которого в качестве логики
  // прописываем вызов приватного метода handler нашей структуры Router.
  // Далее мы разберем что такое handler Handler и rt.handler.
	rt.Mux.Get(path, func(w http.ResponseWriter, r *http.Request) {
		rt.handler(handler, w, r)
	})
}

Handler и rt.handler

Создадим тип Handler, прописав его в самом верху файла router.go:

ROUTER.GO
// Handler обработчик
type Handler func(ctx *Ctx) error

Как вы могли заметить это тот самый обработчик, который похож на обработчики используемые в фреймворках.

Опишем приватный метод handler структуры Router, который принимает в качестве параметров, созданный выше тип Handler.

ROUTER.GO
// handler запускает обработчик роутера
func (rt *Router) handler(handler Handler, w http.ResponseWriter, r *http.Request) {
  // При создании типа Handler мы прописали, что Handler является функцией, которая
  // принимает в качестве параметра контекст и возвращает ошибку: type Handler func(ctx *Ctx) error
  // Данной строкой мы создаем тот самый контекст, который принимает функция типа Handler
  // Мы еще не описывали внутреннюю логику создания контекста, это будет сделано далее.
  ctx := NewCtx(w, r)

  // Важно понимать что handler это тот самый обработчик нашего маршрута, который при использовании стандартного роутера
  // пакета http принимал в качестве параметров w http.ResponseWrite и r *http.Request.
  // Но сейчас наш handler принимает контекст.
  // Выше мы создали контекст ctx := NewCtx(w, r)
  // Теперь мы передаем, созданный контекст в наш обработчик handler(ctx).
  // Сразу не отходя от кассы мы проверяем была ли ошибка,
  // так как наш обработчик должен возвращать ошибку: func(ctx *Ctx) error
  // Если обработчик вернет ошибку мы формируем JSON ответ в который передаем значения
  // success (успешен ли запрос) и message (текст ошибки).
  // Мы так же пока не знакомы с содержимым метода JSON, далее в статье мы непременно его напишем.
  if err := handler(ctx); err != nil {
    err = ctx.JSON(Map{
      "success": false,
      "message": err.Error(),
    })

    // При отправки ответа так же может произойти ошибка, поэтому метод JSON так же возвращает ошибку.
    // Если ошибка произошла, то мы отображаем ее стандартным выводом пакета http.
    if err != nil {
      http.Error(ctx.ResponseWriter, "Не предвиденная ошибка", http.StatusInternalServerError)
    }
  }
}

Пример использования метода Get

Прежде чем идти дальше и описывать код контекста (NewCtx). Для лучшего понимания происходящего напишем обработчик для, созданного метода Get.

В файле _example/main.go к уже имеющемуся коду добавим:

MAIN.GO
route.Get("/profile", func(ctx *chix.Ctx) error {
    // some code

    return nil
  })

Теперь можно проследить логику:

  1. Создается маршрут profile для метода Get, с некоторым обработчиком func(ctx *chix.Ctx) error.
  2. При создании маршрута вызывается метод обертка Get. Который вызывает стандартный метод Get у роутера Chi. В данном методе прописан вызов приватного метода handler нашего Chix роутера.
  3. В приватный метод handler передается обработчик маршрута func(ctx *chix.Ctx) error.
  4. Приватный метод handler создает новый контекст.
  5. Вызывается переданный обработчик как callback функция, которая принимает в качестве параметра, созданный контекст handler(ctx).
  6. При вызове обработчика исполняется его внутренний код func(ctx *chix.Ctx) error { return nil } и возвращается ошибка.
  7. При ошибке отправляется JSON ответ с сообщением об ошибке.
  8. Если при отправке JSON ответа произошла ошибка, то ошибка отобразится стандартным способом отображения ошибок пакета http.

На данный момент мы создали:

  • type Handler func(ctx *Ctx) error
  • type Router struct / func NewRouter() *Router
  • func (rt *Router) handler(handler Handler, w http.ResponseWriter, r *http.Request)
  • func (rt *Router) Get(path string, handler Handler)

Еще предстоит создать множество методов такие как:

  • Post
  • Put
  • Patch
  • Delete
  • NotFound
  • MethodNotAllowed
  • Use
  • Group
  • With
  • Route
  • Mount
  • ServeHTTP
  • Listen

Возможно это не все методы которые предоставляет Chi роутер, перечисленные методы это некая база часто используемых методов при работе с Chi роутером. Если обнаружится, что какой-то метод необходимый вам был пропущен, вы всегда сможете вокруг него создать обертку по аналогии с этой статьей.

Прежде чем продолжить создавать обертки для выше изложенных методов предлагаю разобраться с созданием контекста, который мы описывали ранее ctx := NewCtx(w, r).

Создание контекста

В корне проекта создадим файл context.go:

BASH
touch context.go

Со следующим содержимым:

CONTEXT.GO
package chix

import "net/http"

// Map шаблон для передачи данных
// Данный тип уже был использован когда мы обрабатывали ошибку handler:
// Map{ "success": false, "message": err.Error() }
// Здесь мы его определяем.
type Map map[string]any

// Ctx контекст предоставляемый в обработчик
type Ctx struct {
  // Аналог w http.ResponseWrite
  http.ResponseWriter

  // Аналог r *http.Request
  *http.Request

  // Используется для middleware
  NextHandler http.Handler

  // Хранит статус ответа от сервера
  status int
}

// NewCtx создание контекста
// Здесь вместо обработчика сам контекст принимает запрос, ответ.
func NewCtx(w http.ResponseWriter, r *http.Request) *Ctx {
  return &Ctx{
    ResponseWriter: w,
    Request:        r,

    // Здесь мы получаем следующий обработчик в цепочке middleware
    // Мы еще не знакомились с middleware и внутренней логикой кода NextHandler, это будет сделанно позже.
    NextHandler: NextHandler(r.Context()),

    status: http.StatusOK,
  }
}

При описании обработки ошибки handler(ctx) мы использовали ctx.JSON, опишем этот метод и многие другие необходимые для создания Rest API:

CONTEXT.GO
// Status установка статуса ответа
func (ctx *Ctx) Status(status int) *Ctx {
  ctx.status = status
  return ctx
}

// Header установка заголовка
// Более компактная обертка для создания заголовков.
func (ctx *Ctx) Header(key, value string) {
  ctx.ResponseWriter.Header().Set(key, value)
}

// Decode декодирование тела запроса
// Декодирование json запроса.
func (ctx *Ctx) Decode(data any) error {
  if err := json.NewDecoder(ctx.Request.Body).Decode(data); err != nil {
    ctx.Status(http.StatusBadRequest)

    if errors.Is(err, io.EOF) {
      return errors.New("пустое тело запроса")
    }

    return errors.New("не корректный json")
  }

  return nil
}

// JSON формирование json ответа
// Отправка json ответа.
func (ctx *Ctx) JSON(data Map) error {
  ctx.Header("Content-Type", "application/json")
  ctx.WriteHeader(ctx.status)

  encoder := json.NewEncoder(ctx.ResponseWriter)
  encoder.SetIndent("", "  ")
  if err := encoder.Encode(data); err != nil {
    return err
  }

  return nil
}

Для лучшего понимая, что здесь произошло, вернемся к файлу _example/main.go и напишем следующий код:

MAIN.GO
// Данные запрос, которые ожидаем получить от клиента
type Request struct {
  Name string
  Age  int
}

// Данные ответа, которые хотим отправить клиенту
type Response struct {
  Date time.Time
}

route.Get("/profile", func(ctx *chix.Ctx) error {
  // Запрос
  var req Request
  if err := ctx.Decode(&req); err != nil {
    return ctx.Status(http.StatusBadRequest).JSON(chix.Map{
      "success": false,
      "message": err.Error(),
    })
  }

  // Некая бизнес логика
  var res Response
  res.Date = time.Now()

  // Ответ
  return ctx.JSON(chix.Map{
    "success": true,
    "message": "Время ответа",
    "data":    res,
  })
})

Как видим наш обработчик роутера становится похожим на привычные обработчики из любимых фреймворков.

Создание роутера продолжение

Настала пора вернуться к реализации описанных ранее недостающих методов для нашего роутера:

  • Post
  • Put
  • Patch
  • Delete
  • NotFound
  • MethodNotAllowed
  • Use
  • Group
  • With
  • Route
  • Mount
  • ServeHTTP
  • Listen

Методы Post, Put, Patch, Delete, NotFound, MethodNotAllowed мало чем отличаются от созданного ранее метода Get. Поэтому я не буду вдаваться в подробности как они работают, просто опишу их логику:

MAIN.GO
// Post запрос на добавление данных
func (rt *Router) Post(path string, handler Handler) {
  rt.Mux.Post(path, func(w http.ResponseWriter, r *http.Request) {
    rt.handler(handler, w, r)
  })
}

// Put запрос на обновление всех данных
func (rt *Router) Put(path string, handler Handler) {
  rt.Mux.Put(path, func(w http.ResponseWriter, r *http.Request) {
    rt.handler(handler, w, r)
  })
}

// Patch запрос на обновление конкретных данных
func (rt *Router) Patch(path string, handler Handler) {
  rt.Mux.Patch(path, func(w http.ResponseWriter, r *http.Request) {
    rt.handler(handler, w, r)
  })
}

// Delete запрос на удаление данных
func (rt *Router) Delete(path string, handler Handler) {
  rt.Mux.Delete(path, func(w http.ResponseWriter, r *http.Request) {
    rt.handler(handler, w, r)
  })
}

// NotFound обрабатывает 404 статус
func (rt *Router) NotFound(handler Handler) {
  rt.Mux.NotFound(func(w http.ResponseWriter, r *http.Request) {
    rt.handler(handler, w, r)
  })
}

// MethodNotAllowed обрабатывает 405 статус
func (rt *Router) MethodNotAllowed(handler Handler) {
  rt.Mux.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) {
    rt.handler(handler, w, r)
  })
}

С остальными методами будет поинтересней.

Метод Use

Метод Use добавляет промежуточное программное обеспечение (middleware). Он пересекается с конструкцией NextHandler http.Handler, с которой мы столкнулись при создании структуры контекста:

CONTEXT.GO
type Ctx struct {
  ...
  NextHandler http.Handler
  ...
}

Ниже приведу код метода Use, а затем вернемся к NextHandler:

CONTEXT.GO
// Use добавляет промежуточное программное обеспечение
func (rt *Router) Use(middlewares ...Handler) {
  // Перебираем все полученные middleware
  for _, middleware := range middlewares {
    // Обязательно создаем отдельную переменную, хранящую текущий middleware
    // Это обусловлено особенностью самого языка golang, так как если не сохранить
    // текущую итерацию, то получать в обработчике будем всегда самую последнюю.
    currentMiddleware := middleware

    // Тут вызываем метод Use Chi роутера
    rt.Mux.Use(func(next http.Handler) http.Handler {
      return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Добавляем в текущий контекст следующий обработчик в цепочке middleware
        ctx := r.Context()
        ctx = WithNextHandler(ctx, next)

        // Вызываем метод handler, разобранный ранее в который передаем на этот раз
        // вместо обработчика роута, обработчик middleware.
        // Далее передаем ответ (w) и запрос (r), обратите внимание что r передается по особенному,
        // здесь мы переопределяем контекст в запросе роутера.
        rt.handler(currentMiddleware, w, r.WithContext(ctx))
      })
    })
  }
}

Next Handler

Теперь давайте разберемся что же такое WithNextHandler, для этого в корне проекта создадим файл next_handler.go:

BASH
touch next_handler.go

Со следующим содержимым:

NEXT_HANDLER.GO
package chix

import (
  "context"
  "net/http"
)

// Тип key ключ контекста
type key string

// Значение ключа
const nextKey key = "next"

// WithNextHandler добавление следующего обработчика в цепочке middleware в контекст
func WithNextHandler(ctx context.Context, next http.Handler) context.Context {
  return context.WithValue(ctx, nextKey, next)
}

// NextHandler получение следующего обработчика в цепочке middleware из контекста
func NextHandler(ctx context.Context) http.Handler {
  val := ctx.Value(nextKey)

  next, ok := val.(http.Handler)
  if !ok {
    return nil
  }

  return next
}

При регистрации middleware через метод Use мы добавляем в контекст через функцию WithNextHandler следующий обработчик и возвращаем получившийся контекст с добавленными данными.

При создании нового контекста NewCtx(w, r) мы передаем текущий контекст в функцию NextHandler для того, чтобы получить следующий в цепочке middleware обработчик и сохраняем его в свойстве NextHandler у структуры Ctx.

CONTEXT.GO
func NewCtx(w http.ResponseWriter, r *http.Request) *Ctx {
  return &Ctx{
    ...
    // Cохраняем следующий в цепочке **middleware** обработчик в свойстве **NextHandler**
    NextHandler: NextHandler(r.Context()),
    ...
  }
}

Чтобы полученная информация улеглась в голове, напишем тестовый middleware, чтобы на практике увидеть, то что мы описали.

В каталоге _example создадим файл middleware:

BASH
touch _example/middleware.go

Со следующим содержимым:

MIDDLEWARE.GO
package main

import (
  "github.com/eliofery/go-chix"
)

// Example пример реализации middleware
// Мне нравится создавать middleware именно таким способом, а не напрямую например:
// func Example(ctx *chix.Ctx) error {}
// Так как в middleware при желании можно передать необходимые параметры например:
// Example(foo string, bar int) chix.Handler {}
// Затем внутри middleware ими воспользоваться.
func Example() chix.Handler {
  return func(ctx *chix.Ctx) error {
    // Некая логика
    // Если произошла ошибка, то возвращаем ошибку
    // и цепочка middleware будет прервана.
    if false {
      return errors.New("некая ошибка")
    }

    // При успешной логике вызываем следующий обработчик
    // в цепочке middleware
    return ctx.Next()
  }
}

Вы могли заметить, что метод ctx.Next() нам неизвестен, так как мы его еще не описывали. Самое время исправить этот момент, откроем файл context.go и добавим метод Next.

CONTEXT.GO
// Next обработка следующего обработчика
func (ctx *Ctx) Next() error {
  // ServeHTTP стандартный метод интерфейса http.Handler
  // Произойдет вызов следующего обработчика
  ctx.NextHandler.ServeHTTP(ctx.ResponseWriter, ctx.Request)

  return nil
}

Метод Next позволяет вызвать сохраненный ctx.NextHandler благодаря чему происходит обработка следующего middleware в цепочке.

С методом Use который регистрирует промежуточные программные обеспечения (middleware) покончено. Было не просто понадобится какое-то время, чтобы разобраться со всем, что здесь произошло, а я перехожу к реализации следующего метода.

Метод With

Метод With используется для добавления middleware к группе маршрутов, что облегчает добавление одних и тех же middleware к нескольким маршрутам.

ROUTER.GO
// With добавляет встроенное промежуточное программное обеспечение для обработчика конечной точки
func (rt *Router) With(middlewares ...Handler) *Router {
  // Так как стандартный метод With роутера Chi принимает множество обработчиков func(http.Handler) http.Handler
  // Необходимо смоделировать этот тип данных.
  var handlers []func(http.Handler) http.Handler

  // Далее код аналогичен тому, что мы прописывали в методе Use за исключением того,
  // что мы сохраняем обработчик в массиве, который создали выше
  for _, middleware := range middlewares {
    currentMiddleware := middleware

    handler := func(next http.Handler) http.Handler {
      return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        ctx = WithNextHandler(ctx, next)

        rt.handler(currentMiddleware, w, r.WithContext(ctx))
      })
    }

    handlers = append(handlers, handler)
  }

  // Создаем новый роутер используя переданные middlewares
  return &Router{
    Mux: rt.Mux.With(handlers...).(*chi.Mux),
  }
}

Опишем пример использования метода With, для этого откроем файл _example/main.go и добавим:

MAIN.GO
route.With(Example()).Route("/group", func(r *chix.Router) {
  r.Get("/route1", func(ctx *chix.Ctx) error { return nil })
  r.Get("/route2", func(ctx *chix.Ctx) error { return nil })
})

Мы еще не определили метод Route давайте этим и займемся.

Метод Route

Метод Route создает вложенность роутеров.

MAIN.GO
// Route создает вложенность роутеров
func (rt *Router) Route(pattern string, fn func(r *Router)) *Router {
  // Создаем дочерний роутер
  subRouter := &Router{
    Mux: chi.NewRouter(),
  }

  // Передаем его внутрь атрибута fn
  fn(subRouter)

  // Mount добавляет вложенность роутеров друг в друга.
  // В данном случае мы вкладываем внутрь родительского роута rt дочерний роут subRouter.
  // Мы еще не описывали метод Mount, займемся этим позже.
  rt.Mount(pattern, subRouter)

  return subRouter
}

После добавления метода Route текстовый редактор должен перестать ругаться на пример описанный ваше в файле main.go.

Метод Mount

Метод Mount добавляет вложенность роутеров друг в друга.

MAIN.GO
// Mount добавляет вложенность роутеров
func (rt *Router) Mount(pattern string, router *Router) {
  // Здесь мы так же создаем обертку для того, чтобы использовать нашу структуру роутера
  // вместо Chi роутера.
  rt.Mux.Mount(pattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Запускаем наш роут
    router.Mux.ServeHTTP(w, r)
  }))
}

Метод Group

Метод Group позволяет группировать роутеры.

ROUTER.GO
// Group группирует роутеры
func (rt *Router) Group(fn func(r *Router)) *Router {
  // Создаем роут с привязкой middlewares
  // Возможно можно было ограничиться лишь переменной rt, например: fn(rt),
  // но я не уверен, что это сработает, так как сам Chi использует подход
  // с созданием роута через метод With.
	im := rt.With()

	if fn != nil {
		fn(im)
	}

	return im
}

Львиная часть методов позади осталось всего нечего.

Метод ServeHTTP

Метод ServeHTTP возвращает весь пул роутеров.

ROUTER.GO
// ServeHTTP возвращает весь пул роутеров
func (rt *Router) ServeHTTP() http.HandlerFunc {
  // Здесь ни чего особенного просто возвращаем стандартный http.HandlerFunc
  return rt.Mux.ServeHTTP
}

Метод Listen

Метод Listen запускает сервер. Является локомотивом нашего роутера без которого ни чего не заработает.

ROUTER.GO
// Listen запускает сервер
// Реализация: https://github.com/go-chi/chi/blob/master/_examples/graceful/main.go
func (rt *Router) Listen(addr string) error {
  // Создаем сервер
  server := &http.Server{
    Addr:    addr,
    Handler: rt.ServeHTTP(),
  }

  // Подписываемся на сигналы операционной системы, в данном случе на сигнал os.Interrupt,
  // который вызывается ОС при нажатии на клавиши Ctrl + C.
  ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
  defer cancel()

  // Создаем канал для ошибок
  ch := make(chan error, 1)

  // Запускаем сервер в горутине
  go func() {
    if err := server.ListenAndServe(); err != nil {
      // Если возникла ошибка при запуске сервера отправляем ошибку в канал
      if !errors.Is(err, http.ErrServerClosed) {
        fmt.Printf("Не удалось запустить сервер: %s", err.Error())
        ch <- ctx.Err()
      }
    }
    close(ch)
  }()

  // Слушаем каналы
  select {
  // При ошибке запуска сервера
  case err := <-ch:
    panic(err)
  // При ошибки завершения работы сервера
  case <-ctx.Done():
    // Создаем таймер в течении 10 сек. сервер должен завершить свою работу при
    // Нажатии на Ctrl + C
    timeoutCtx, done := context.WithTimeout(context.Background(), time.Second*10)
    defer done()

    // Ловим завершение работы с сервером
    go func() {
      <-timeoutCtx.Done()
      if errors.Is(timeoutCtx.Err(), context.DeadlineExceeded) {
        fmt.Printf("Время корректного завершения работы истекло. Принудительный выход: %s", timeoutCtx.Err().Error())
      }
    }()

    // Завершаем работу с сервером
    if err := server.Shutdown(timeoutCtx); err != nil {
      fmt.Printf("Не удалось остановить сервер: %s", err.Error())
    }
  }

  return nil
}

Использование сторонних middleware

Мы разобрали как в рамках нашего роутера создается middleware. Но бывают моменты когда необходимо использовать готовые middleware под роутер Chi и тут возникает нюанс. Который разберем в данном разделе на примере middleware Cors.

Скачаем пакет github.com/go-chi/cors:

ROUTER.GO
go get github.com/go-chi/cors

В каталоге _example создадим файл cors.go со следующим содержимым:

ROUTER.GO
package main

import (
	"github.com/eliofery/go-chix"
	"github.com/go-chi/cors"
)

const defaultCorsMaxAge = 3600 // 1 час

// Cors настройки межсайтового взаимодействия
// Пример: https://github.com/go-chi/cors?tab=readme-ov-file#usage
func Cors() chix.Handler {
  // Используем обработчик нашего роутера
  return func(ctx *chix.Ctx) error {
    // Создаем Cors обработчик сохраняя его в переменной
    corsHandler := cors.Handler(cors.Options{
      AllowedOrigins:   []string{"http://localhost:3000"},
      AllowedMethods:   []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
      AllowedHeaders:   []string{"Origin", "Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
      ExposedHeaders:   []string{"Link", "Content-Length", "Access-Control-Allow-Origin"},
      AllowCredentials: true,
      MaxAge:           defaultCorsMaxAge,
    })

    // Передаем в cors обработчик следующий обработчик и выполняем его.
    // Далее если внутри логики cors возникнет ошибка corsHandler сам прервет цепочку middleware.
    // Нам нет надобности вызывать ctx.Next()
    corsHandler(ctx.NextHandler).ServeHTTP(ctx.ResponseWriter, ctx.Request)

    return nil
  }
}

Вот таким нехитрым образом мы подружили стандартный Chi middleware с нашим роутером.

Подведем итоги

В этой статье мы создали свою обертку над Chi роутером благодаря которой наш опыт использования этого маршрутизатора будет похож на использование привычных фреймворков на подобии gin и fiber.

Ссылки на проект

Предыдущая статья Автоматическая компиляция Golang проекта при изменении файлов Следующая статья Создание сервера Golang с gRPC и Rest API используя Swagger