Оглавление статьи
- Вводная часть
- Подготовительные работы
- Создание роутера
- Создание контекста
- Создание роутера продолжение
- Использование сторонних middleware
- Подведем итоги
Вводная часть
В работе со стандартным роутером в Golang, обработчикам в качестве параметров передается запрос, ответ:
// http
func Handler(w http.ResponseWrite, r *http.Request) {
// code
}
В работе с различными фреймворками на языке Golang, при создании обработчиков используется паттерн, где в качестве параметра передается context, например:
// Gin
func Handler(ctx *gin.Context) {
// code
}
// Fiber
func Handler(ctx *fiber.Ctx) error {
// code
}
Подход с контекстом является более гибким, он позволяет передавать помимо стандартного запроса, ответа еще и другие необходимые данные.
В этой статье мы создадим обертку, которая позволит создавать аналогичный с приведенными примерами обработчик.
// Chix
func Handler(ctx *chix.Ctx) error {
// code
}
Подготовительные работы
Первым делом необходимо инициализировать наш пакет (не забудьте изменить ссылку на репозиторий и название пакета на свои):
go mod init github.com/eliofery/go-chix
Далее установим пакет роутера Chi на основе которого мы будем создавать свою обертку:
go get -u github.com/go-chi/chi/v5
После проделанных манипуляций создастся файл 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:
touch router.go
Внутри созданного файла создадим структуру нашего будущего роутера:
package chix
import "github.com/go-chi/chi/v5"
// Router обертка над chi роутером
type Router struct {
*chi.Mux
}
В структуре нашего роутера мы используем *chi.Mux которая является Chi роутером.
Далее создадим конструктор для нашей структуры Router:
// NewRouter создание роутера
func NewRouter() *Router {
return &Router{
Mux: chi.NewRouter(),
}
}
При создании нашего роутера создается новый роутер Chi, который мы будем использовать в описании своего обработчика.
В корне проекта создадим каталог _example и файл main.go внутри него:
mkdir _example
touch _example/main.go
Со следующим содержимым:
package main
import "github.com/eliofery/go-chix"
func main() {
// Инициализация нашего роутера
route := chix.NewRouter()
}
Теперь когда роутер, который является оберткой над роутером Chi создан, мы можем приступить к описанию методов.
Метод Get
Первый метод который мы опишем, будет метод Get. Рассмотрим подробнее, что происходит:
// 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:
// Handler обработчик
type Handler func(ctx *Ctx) error
Как вы могли заметить это тот самый обработчик, который похож на обработчики используемые в фреймворках.
Опишем приватный метод handler структуры Router, который принимает в качестве параметров, созданный выше тип Handler.
// 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 к уже имеющемуся коду добавим:
route.Get("/profile", func(ctx *chix.Ctx) error {
// some code
return nil
})
Теперь можно проследить логику:
- Создается маршрут profile для метода Get, с некоторым обработчиком func(ctx *chix.Ctx) error.
- При создании маршрута вызывается метод обертка Get. Который вызывает стандартный метод Get у роутера Chi. В данном методе прописан вызов приватного метода handler нашего Chix роутера.
- В приватный метод handler передается обработчик маршрута func(ctx *chix.Ctx) error.
- Приватный метод handler создает новый контекст.
- Вызывается переданный обработчик как callback функция, которая принимает в качестве параметра, созданный контекст handler(ctx).
- При вызове обработчика исполняется его внутренний код func(ctx *chix.Ctx) error { return nil } и возвращается ошибка.
- При ошибке отправляется JSON ответ с сообщением об ошибке.
- Если при отправке 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:
touch 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:
// 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 и напишем следующий код:
// Данные запрос, которые ожидаем получить от клиента
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. Поэтому я не буду вдаваться в подробности как они работают, просто опишу их логику:
// 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, с которой мы столкнулись при создании структуры контекста:
type Ctx struct {
...
NextHandler http.Handler
...
}
Ниже приведу код метода Use, а затем вернемся к NextHandler:
// 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:
touch 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.
func NewCtx(w http.ResponseWriter, r *http.Request) *Ctx {
return &Ctx{
...
// Cохраняем следующий в цепочке **middleware** обработчик в свойстве **NextHandler**
NextHandler: NextHandler(r.Context()),
...
}
}
Чтобы полученная информация улеглась в голове, напишем тестовый middleware, чтобы на практике увидеть, то что мы описали.
В каталоге _example создадим файл middleware:
touch _example/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.
// 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 к нескольким маршрутам.
// 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 и добавим:
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 создает вложенность роутеров.
// 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 добавляет вложенность роутеров друг в друга.
// 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 позволяет группировать роутеры.
// 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 возвращает весь пул роутеров.
// ServeHTTP возвращает весь пул роутеров
func (rt *Router) ServeHTTP() http.HandlerFunc {
// Здесь ни чего особенного просто возвращаем стандартный http.HandlerFunc
return rt.Mux.ServeHTTP
}
Метод Listen
Метод Listen запускает сервер. Является локомотивом нашего роутера без которого ни чего не заработает.
// 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:
go get github.com/go-chi/cors
В каталоге _example создадим файл cors.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.