Введение
Соглас��о официальному сайту, 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 для сбора статистики по обработчикам).
Свои идеи, предложения и вопросы пишите в комментарии или мне в телеграм.
