Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

CRUD-операции в Fiber Веб-разработка на Go

В этом уроке мы научимся разрабатывать веб-приложение, которое выполняет различные операции над данными в хранилище: создание, чтение, обновление и удаление. Для веб-разработки это важная тема, потому что почти все веб-приложения выполняют эти операции.

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

CRUD-операции

Любой объект в хранилище можно представить в виде ресурса. С ресурсом можно работать посредством следующих операций:

  • Создание — create
  • Чтение — read
  • Обновление — update
  • Удаление — delete

По первым буквам английских названий этих операций образуется акроним CRUD. Так как в веб-приложениях взаимодействие происходит по HTTP, нужно сопоставить операции CRUD с этим протоколом. Обычно для каждой операции используют свой HTTP-метод:

  • C (create) — POST
  • R (read) — GET
  • U (update) — PATCH/PUT
  • D (delete) — DELETE

Например, мы разрабатываем систему управления сотрудников компании. Ресурс в данной системе — сотрудник. Путь до него мы определяем как /employees, а CRUD будет выглядеть следующим образом:

  • C (create) — POST /employees
  • R (read) — GET /employees или GET /employees/:id
  • U (update) — PATCH/PUT /employees/:id
  • D (delete) — DELETE /employees/:id

Каждый метод содержит идентификатор сотрудника, кроме метода создания. Это связано с тем, что когда сотрудник создается, его еще нет в хранилище. Поэтому его идентификатор неизвестен.

Мы определили, как будут выглядеть CRUD-операции на протоколе HTTP. Теперь рассмотрим, как реализовать это в микрофреймворке Fiber.

CRUD-операции в Fiber

В Fiber каждый HTTP-метод представлен своей функцией. Чтобы реализовать CRUD-операции в веб-приложении, мы будем использовать следующие функции:

package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/sirupsen/logrus"
)

func main() {
    webApp := fiber.New()

    // Создание сотрудника
    webApp.Post("/employees", ...)

    // Получение сотрудника
    webApp.Get("/employees/:id", ...)

    // Обновление сотрудника
    webApp.Patch("employees/:id", ...)

    // Удаление сотрудника
    webApp.Delete("employees/:id", ...)

    logrus.Fatal(webApp.Listen(":80"))
}

Для каждой CRUD-операции описывается уникальный обработчик. Каждый обработчик выполняет конкретную маленькую задачу и не должен содержать в себе логики, которая не относится к этой операции. Такое построение веб-приложения позволит легко масштабировать и поддерживать код.

Для простоты представим, что объект сотрудника содержит только идентификатор, электронную почту и роль. Хранить данные будем в оперативной памяти приложения с помощью структуры данных map:

type Employee struct{
    ID string
    Email string
    Role string
}

type MemoryEmployeeStorage struct{
    employees map[string]Employee
}

Разберем каждую CRUD-операцию подробнее.

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

Изначально в хранилище нет сотрудников, поэтому первым делом нам нужно реализовать метод его создания. Для этого используется метод POST /employees, в котором передаются все данные нового сотрудника:

package main

import (
    "fmt"
    "github.com/gofiber/fiber/v2"
    "github.com/google/uuid"
    "github.com/sirupsen/logrus"
)

// Создание сотрудника
type (
    CreateEmployeeRequest struct {
        Email string `json:"email"`
        Role  string `json:"role"`
    }

    CreateEmployeeResponse struct {
        ID string `json:"id"`
    }
)

// Хранилище
type (
    Employee struct {
        ID    string
        Email string
        Role  string
    }

    EmployeeStorageInMemory struct {
        employees map[string]Employee
    }
)

func (s *EmployeeStorageInMemory) Create(empl Employee) (string, error) {
    // Генерируем ID для сотрудника
    empl.ID = uuid.New().String()

    s.employees[empl.ID] = empl

    return empl.ID, nil
}

func main() {
    webApp := fiber.New()

    storage := &EmployeeStorageInMemory{
        employees: make(map[string]Employee),
    }

    // Создание сотрудника
    webApp.Post("/employees", func(ctx *fiber.Ctx) error {
        // Парсим JSON-тело запроса в объект CreateEmployeeRequest
        var req CreateEmployeeRequest
        if err := ctx.BodyParser(&req); err != nil {
            return fmt.Errorf("body parser: %w", err)
        }

        // Сохраняем объект сотрудника в хранилище
        // Метод Create возвращает ID созданного сотрудника
        id, err := storage.Create(Employee{
            Email: req.Email,
            Role:  req.Role,
        })
        if err != nil {
            return fmt.Errorf("create in storage: %w", err)
        }

        // Возвращаем ID сотрудника JSON-строкой в теле ответа
        return ctx.JSON(CreateEmployeeResponse{ID: id})
    })

    logrus.Fatal(webApp.Listen(":80"))
}

Запускаем веб-приложение и отправляем запрос на создание нового сотрудника:

curl --location --request POST 'http://localhost/employees' \
--header 'Content-Type: application/json' \
--data-raw '{"email": "john@corp.com", "role": "salesman"}'

В ответ получаем идентификатор созданного сотрудника:

HTTP/1.1 200 OK

{"id":"fad6ff8c-5e6a-4545-9b41-3dc330ce146f"}

Мы отправили POST /employees запрос с данными нового сотрудника. Веб-приложение определило обработчик этого запроса, прочитало тело запроса и сохранило данные нового сотрудника в оперативной памяти. В методе создания нового сотрудника в хранилище storage.Create() генерируется идентификатор сотрудника, по которому в будущем будет происходить поиск сотрудника в хранилище.

set

Идентификатор сотрудника представлен в виде UUID — универсальный уникальный идентификатор. Это гарантирует, что идентификаторы не будут повторяться при большом количестве сотрудников. Генерация UUID стандартизирована, и в Go есть готовая библиотека для генерации такого идентификатора. Для этого используется функция uuid.New().String().

Мы можем создавать сотрудников, но пока не можем получить то, что мы создали. Чтобы решить этот недочет, нам нужен метод чтения.

Чтение данных сотрудников

Метод чтения разделяется на два типа:

  • Получение всех сотрудников. Для этого используется метод GET /employees
  • Получение конкретного сотрудника. Для этого используется метод GET /employees/:id, где :id — это идентификатор сотрудника

Для этих операций мы описываем новые объекты ответов:

// Чтение сотрудника
type (
    ListEmployeesResponse struct {
        Employees []EmployeePayload `json:"employees"`
    }

    GetEmployeeResponse struct {
        EmployeePayload
    }

    EmployeePayload struct {
        ID    string `json:"id"`
        Email string `json:"email"`
        Role  string `json:"role"`
    }
)
    // Получение списка сотрудников
    webApp.Get("/employees", func(ctx *fiber.Ctx) error {
        // Получаем список всех сотрудников из хранилища
        employees := storage.List()

        // Формируем ответ
        resp := ListEmployeesResponse{
            Employees: make([]EmployeePayload, len(employees)),
        }
        for i, empl := range employees {
            resp.Employees[i] = EmployeePayload(empl)
        }

        // Возвращаем список сотрудников JSON-строкой в теле ответа
        return ctx.JSON(resp)
    })

    // Получение одного сотрудника
    webApp.Get("/employees/:id", func(ctx *fiber.Ctx) error {
        empl, err := storage.Get(ctx.Params("id"))
        if err != nil {
            return fiber.ErrNotFound
        }

        // Возвращаем данные сотрудника JSON-строкой в теле ответа
        return ctx.JSON(GetEmployeeResponse{EmployeePayload(empl)})
    })

Также мы добавляем две функции в хранилище, чтобы получить данные сотрудников:

func (s *EmployeeStorageInMemory) List() []Employee {
    // Инициализируем массив с размером равным количеству
    // всех сотрудников в хранилище
    employees := make([]Employee, 0, len(s.employees))

    for _, empl := range s.employees {
        employees = append(employees, empl)
    }

    return employees
}

func (s *EmployeeStorageInMemory) Get(id string) (Employee, error) {
    empl, ok := s.employees[id]
    if !ok {
        // Возвращаем ошибку, если сотрудника с таким
        // идентификатором не существует
        return Employee{}, errors.New("employee not found")
    }

    return empl, nil
}

Запускаем веб-приложения и попробуем прочесть данные. Чтобы протестировать получение сотрудников, нам нужно сначала создать пару записей:

curl --location --request POST 'http://localhost/employees' \
--header 'Content-Type: application/json' \
--data-raw '{"email": "john@corp.com", "role": "salesman"}'
curl --location --request POST 'http://localhost/employees' \
--header 'Content-Type: application/json' \
--data-raw '{"email": "jane@corp.com", "role": "CEO"}'

На запросы получаем ответы соответственно:

{"id":"e8f3b261-8b0b-4fb2-b5dd-8714b72a01a6"}%
{"id":"6972cc2c-50e0-400c-808f-deb63f37c4f1"}%

Теперь попробуем получить список всех сотрудников:

curl --location --request GET 'http://localhost/employees'
{
   "employees":[
      {
         "id":"e8f3b261-8b0b-4fb2-b5dd-8714b72a01a6",
         "email":"john@corp.com",
         "role":"salesman"
      },
      {
         "id":"6972cc2c-50e0-400c-808f-deb63f37c4f1",
         "email":"jane@corp.com",
         "role":"CEO"
      }
   ]
}

Также проверим метод получения одного сотрудника:

curl --location --request GET 'http://localhost/employees/6972cc2c-50e0-400c-808f-deb63f37c4f1'
{
   "id":"6972cc2c-50e0-400c-808f-deb63f37c4f1",
   "email":"jane@corp.com",
   "role":"CEO"
}

Мы реализовали обработчик запросов на чтение данных сотрудников. Когда мы отправили запрос GET /employees, веб-приложение определило обработчик и вернуло все записи из хранилища в JSON-виде.

Если указать идентификатор сотрудника при запросе GET /employees/:id, то веб-приложение вернет данные только одного сотрудника. Мы также учли, что в хранилище может не существовать сотрудник с таким идентификатором. В этом случае веб-приложение вернет ошибку 404.

Обновление сотрудника

Мы научились создавать и читать данные сотрудников, но пока не умеем их обновлять. Например, сотрудник может перейти на другую должность в компании. В этом случае будет отправляться запрос на обновление PATCH /employees/:id с новым значением поля Role.

Реализуем обработчик обновления. Начнем с описания запроса на обновление:

// Обновление сотрудников
type (
    UpdateEmployeeRequest struct {
        Email string `json:"email"`
        Role  string `json:"role"`
    }
)

Теперь опишем обработчик запроса на обновление:

// Получение списка сотрудников
webApp.Patch("/employees:id", func(ctx *fiber.Ctx) error {
    // Парсим JSON-тело запроса в объект UpdateEmployeeRequest
    var req UpdateEmployeeRequest
    if err := ctx.BodyParser(&req); err != nil {
        return fmt.Errorf("body parser: %w", err)
    }

    // Обновляем данные сотрудника в хранилище. Эта функция может вернуть ошибку, 
    // если сотрудника с таким идентификатором не существует.
    err = storage.Update(ctx.Params("id"), req.Email, req.Role)
    if err != nil {
        return fmt.Errorf("update: %w", err)
    }

    return nil
})

Далее опишем метод обновления сотрудника в хранилище:

func (s *EmployeeStorageInMemory) Update(id, email, role string) error {
    empl, ok := s.employees[id]
    if !ok {
        // Возвращаем ошибку, если сотрудника с таким
        // идентификатором не существует
        return errors.New("employee not found")
    }

    // Обновляем электронную почту сотрудника,
    // если новое значение было передано
    if email != "" {
        empl.Email = email
    }
    // Обновляем роль сотрудника,
    // если новое значение было передано
    if role != "" {
        empl.Role = role
    }

    s.employees[empl.ID] = empl

    return nil
}

Запускаем веб-приложение и создаем нового сотрудника:

curl --location --request POST 'http://localhost/employees' \
--header 'Content-Type: application/json' \
--data-raw '{"email": "john@corp.com", "role": "salesman"}'
{"id":"4e98eb9c-13c1-41a2-8b1e-945387df98cb"}

Теперь обновим данные этого сотрудника:

curl --location --request PATCH 'http://localhost/employees/4e98eb9c-13c1-41a2-8b1e-945387df98cb' \
--header 'Content-Type: application/json' \
--data-raw '{"email": "johnv2@corp.com", "role": "CTO"}'

В ответе получаем пустую строку с кодом 204 No Content, что означает — запрос обработан успешно.

Попробуем получить данные этого сотрудника:

curl --location --request GET 'http://localhost/employees/4e98eb9c-13c1-41a2-8b1e-945387df98cb'

И мы видим, что данные сотрудника обновились:

{
   "id":"4e98eb9c-13c1-41a2-8b1e-945387df98cb",
   "email":"johnv2@corp.com",
   "role":"CTO"
}

Когда мы отправили запрос на обновление PATCH /employees/:id, веб-приложение верно определило обработчик, нашло сотрудника по идентификатору и обновило его данные в хранилище.

Таким образом мы написали почти все операции над ресурсом employees. Осталось только реализовать удаление сотрудника.

Удаление сотрудника

Со временем сотрудники могут увольняться из компании. В этом случае у нас должен быть метод удаления сотрудника из хранилища. Удаление происходит с помощью метода DELETE /employees/:id, где :id — это идентификатор сотрудника.

Для начала опишем функцию удаления сотрудника из хранилища. В нашем случае она состоит из одной строки:

func (s *EmployeeStorageInMemory) Delete(id string) {
    delete(s.employees, id)
}

Теперь добавим обработчик для метода DELETE /employees/:id:

    // Удаление сотрудника
    webApp.Delete("/employees/:id", func(ctx *fiber.Ctx) error {
        storage.Delete(ctx.Params("id"))

        // Возвращаем успешный ответ без тела
        return ctx.SendStatus(fiber.StatusNoContent)
    })

Запускаем веб-приложения и создаем нового сотрудника:

curl --location --request POST 'http://localhost/employees' \
--header 'Content-Type: application/json' \
--data-raw '{"email": "john@corp.com", "role": "salesman"}'

В ответ получаем идентификатор сотрудника:

{"id":"c27bd23f-aa14-47e1-8422-75e18a2eecab"}

Теперь проверим метод удаления:

curl --location --request DELETE 'http://localhost/employees/c27bd23f-aa14-47e1-8422-75e18a2eecab'

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

curl --location --request GET 'http://localhost/employees/c27bd23f-aa14-47e1-8422-75e18a2eecab'

В ответ получаем ожидаемую ошибку, что сотрудник не найден:

HTTP/1.1 404 Not Found

Not Found

Таким образом мы реализовали последнюю CRUD-операцию — удаление сотрудника. Когда мы отправили запрос DELETE /employees/:id, веб-приложение по идентификатору удалило сотрудника из хранилища.

Выводы

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

  • CRUD — это аббревиатура, которая означает Create, Read, Update, Delete
  • Построение веб-приложений по CRUD-модели распространено благодаря простоте разработки и поддержки кода
  • Со стороны HTTP-протокола CRUD-модель реализуется с помощью методов GET, POST, PATCH/PUT, DELETE

Дополнительные материалы

  1. CRUD
  2. UUID

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»
Изображение Тото

Задавайте вопросы, если хотите обсудить теорию или упражнения. Команда поддержки Хекслета и опытные участники сообщества помогут найти ответы и решить задачу