Введение
Согласно официальному сайту, chi — это легковесный, идиоматический и композируемый маршрутизатор для создания HTTP-сервисов на Go. Он на 100% совместим с net/http
и довольно легок в обращении, однако его документация предназначена скорее для опытных разработчиков, чем для новичков, поэтому я решил написать серию статей, в ходе которых мы будем постепенно развивать и перерабатывать простейший CRUD, написанный на chi.
В рамках данной части мы напишем код, который ляжет в основу дальнейших статей. Это будет простой и в чем-то даже грязный код, но это сделано умышленно, чтобы автор вместе с читателем мог прогрессировать от части к части. Код для каждой последующей части будет появляться по мере написания в этом репозитории и помещаться в отдельную ветку, а весь код, написанный для этой части, находится в этой ветке.
Подготовка
Наш CRUD будет обслуживать хранение и обработку следующей структуры:
type CrudItem struct {
Id int
Name string
Description string
internal string
}
За хранение записей будут отвечать следующие две переменные:
currentId := 1
storage := make(map[int]CrudItem)
Сущности мы будем сохранять в карте/словаре (вам как больше нравится?). При необходимости добавить значение в хранилище, оно добавляется по ключу currentId
. Я хочу подчеркнуть, что это решение с запахом и не предназначено для использования в реальных проектах. В следующих частях мы отрефакторим механизм хранения, вынесем его за интерфейс и сделаем его потокобезопасным (но не сегодня).
CRUD
Простейшая программа с использованием chi будет выглядеть так:
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
http.ListenAndServe(":3000", r)
}
Она ничего не делает, кроме создания структуры маршрутизатора и запуска его обслуживания на трехтысячном порту.
Создание простейшего обработчика и навешивание его на паттерн пути в chi выглядит следующим образом:
Выбрать метод марштрутизатора, соответствующий необходимому HTTP-методу
Передать в него паттерн пути и обработчик
http.HandlerFunc
(функция с сигнатуройfunc(w http.ResponseWriter, r *http.Request)
). Из коробки нам доступны следующие HTTP-методы:
Connect(pattern string, h http.HandlerFunc)
Delete(pattern string, h http.HandlerFunc)
Get(pattern string, h http.HandlerFunc)
Head(pattern string, h http.HandlerFunc)
Options(pattern string, h http.HandlerFunc)
Patch(pattern string, h http.HandlerFunc)
Post(pattern string, h http.HandlerFunc)
Put(pattern string, h http.HandlerFunc)
Trace(pattern string, h http.HandlerFunc)
Этого достаточно для написания стандартного CRUD-а, но если вам необходимо написать обработчик собственного кастомного HTTP-метода, то вам сначала необходимо зарегистрировать его с помощью chi.RegisterMethod("JELLO")
, а затем навесить на паттерн пути в маршрутизаторе обработчик с помощью r.Method("JELLO", "/path", myJelloMethodHandler)
.
Create
Код регистрации обработчика для добавления нового CrudItem
в наше импровизированное хранилище выглядит следующим образом:
r.Post("/crud-items/", func(w http.ResponseWriter, r *http.Request) {
var item CrudItem
err := json.NewDecoder(r.Body).Decode(&item)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
item.Id = currentId
storage[currentId] = item
jsonItem, err := json.Marshal(item)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Write(jsonItem)
currentId += 1
})
Что из себя представляет наша реализация обработчика:
Пытаемся прочитать из тела запроса json и десериализовать его в структуру
CrudItem
. Валидный JSON выглядит так:
{
"name": "New name",
"description": "New description"
}
Если по какой-то причине нам не удалось это сделать, мы говорим пользователю о том, что с его запросом что-то не так и заканчиваем работу.
Присваиваем сущности
Id
и сохраняем в наше хранилище. Ходят легенды, что в хороших CRUD-ах принято возвращать добавленный объект с присвоенными ему идентификаторами, и мы поступаем так же:Сериализуем структуру
CrudItem
в json;В случае провала говорим пользователю, что что-то пошло не так по нашей вине;
В случае успеха отправляем пользователю json и инкрементим текущий
Id
.
Read
Чтение мы сделаем двумя обработчиками:
Прочитать все записи;
Прочитать конкретную запись. Ниже приведен обработчик для получения всех сохраненных записей, но пока он нам не интересен -- он нужен нам для следующих частей:
r.Get("/crud-items/", func(w http.ResponseWriter, r *http.Request) {
result := make([]CrudItem, 0, len(storage))
for _, item := range storage {
result = append(result, item)
}
resultJson, err := json.Marshal(result)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Write(resultJson)
})
Гораздо интереснее выглядит обработчик получения записи по Id
:
r.Get("/crud-items/{id}", func(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
if _, ok := storage[id]; !ok {
w.WriteHeader(http.StatusNotFound)
return
}
resultJson, err := json.Marshal(storage[id])
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Write(resultJson)
})
Здесь мы воспользовались получением id
записи из URL. Для этого мы:
Задали в паттерне пути именной параметр
id
с помощью{id}
;С помощью
chi.URLParam(r, "id")
получили строковое значение параметраid
;Попробовали привести параметр
id
к целому числу и в случае провала сообщили пользователю, что с его запросом что-то не так.
Update
Объединив реализации обработчика для добавления новой записи и получения записи по id
мы можем соорудить обработчик для обновления записи:
r.Put("/crud-items/{id}", func(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
if _, ok := storage[id]; !ok {
w.WriteHeader(http.StatusNotFound)
return
}
var item CrudItem
err = json.NewDecoder(r.Body).Decode(&item)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
item.Id = id
storage[id] = item
jsonItem, err := json.Marshal(item)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Write(jsonItem)
})
Delete
Удаление записи из нашего хранилища выглядит следующим образом:
r.Delete("/crud-items/{id}", func(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
if _, ok := storage[id]; !ok {
w.WriteHeader(http.StatusNotFound)
return
}
delete(storage, id)
})
По сути, удаление записи это удаление элемента словаря с предварительной проверкой наличия элемента.
Что дальше?
На этом создание базового приложения заканчивается. Сегодня мы реализовали CRUD с 5-ю обработчиками, используя маршрутизатор chi, научились читать json из тела запроса, отправлять его в ответ и получать значение из паттерна пути.
Чему будут посвящены следующие статьи:
Рефакторинг хранилища и вынос его за интерфейс;
Пагинация для обработчика получения всех записей с использованием middleware;
Использование интерфейса
Renderer
и создание нормальных DTO;Добавление логирования;
Авторизация;
Работа с prometeus (создание обработчика и написание middleware для сбора статистики по обработчикам).
Свои идеи, предложения и вопросы пишите в комментарии или мне в телеграм.
Комментарии (7)
manyakRus
17.03.2024 10:20https://github.com/ManyakRus/crud_generator
а вот мой CRUD + GRPC генератор кода,
который весь этот код сам напишет за вас :-)
SUNsung
И в чем преймущество chi перед другими (тот же gin)?
По коду не увидел особой разницы - если обернуть все руками (стандартный net плюс любой самописный парсер адреса) то особо количество и качество кода не выростет
Brom95 Автор
Честно говоря, с gin особо не работал, но как мне видится, chi немного проще,например, в приеме входных моделей с валидацией. В chi это делается через имплементацию интерфейса
Binder
у самой модели, в то время, как у gin это выглядит несколько сложнее: https://gin-gonic.com/docs/examples/custom-validators/Это потому что я написал пока максимально просто, чтобы был простор для рефакторинга.
Думаю, все
net/http
совместимые роутеры выглядят похожеSUNsung
В gin валидации нет как таковой. И как по мне это правильно - не смешивать мух и котлеты
floordiv
зачем это делать? В чем смысл писать свой роутер каждый раз? Еще и парсер адреса (что бы это ни значило). Вам не лень потом сидеть и покрывать это каждый раз тестами?
SUNsung
Я так понимаю вы из тех, кто считает что модули растут на гитхабе как трава в полях?)
Написать единый метод который в ответ тебе вернет адрес и вложеные в него параметры
В зависимости от задачи и архитектуры можно и парой строк обойтись
.
Писать каждый случай с нуля копипастом это феерический идиотизм.
Если програмист не знает как переиспользовать код и как создавать унитарные методы для разных задач, то ему нечего делать в програмировании (максимум фронт-енд, и то базовый только)
Mi7teR
у chi перед gin есть преимущество в совместимости с net/http