Зарегистрируйтесь, чтобы продолжить обучение

Хранение данных в памяти Go-приложения Веб-разработка на Go

В большинстве случаев веб-приложения взаимодействуют с хранилищами данных. Хранилищем может быть любая база данных, объектное хранилище, файловая система. У каждого есть свой интерфейс взаимодействия. Это большая тема для изучения, поэтому мы опишем базовый принцип работы с хранилищем в веб-приложениях. Для этого воспользуемся хранением данных в памяти Go-приложения.

Хранение данных в памяти Go-приложения

Один из подходов построения архитектуры веб-приложения на Go — это слоеная архитектура. Когда в приложение приходит запрос, он попадает в слой обработчика, далее идет в слой бизнес-логики, а затем в слой хранилища. Если обработчику нужно только прочитать/записать данные из хранилища, то можно перейти в этот слой напрямую из обработчика:

set

Разберем слой хранения данных на примере заказов в интернет-магазине. Мы реализуем веб-приложение, которое позволит создавать новый заказ и получать о нем информацию.

Веб-приложение будет представлено в виде слоеной архитектуры. Каждый слой будет представлен в виде отдельной структуры. Структура OrderHandler выступит в роли обработчика HTTP-запросов, а структура OrderStorage — в роли хранилища заказов.

Опишем хранилище OrderStorage. Для простоты мы будем хранить всю информацию о заказах в памяти приложения. Для этого используем структуру данных map. Ключом будет идентификатор заказа, а значениями — структуры заказов. Такая архитектура позволит получать и записывать значения за алгоритмическую сложность O(1):

type OrderStorage struct {
    orders map[string]Order
}

Структура заказа содержит идентификатор, идентификатор пользователя и список товаров. Для простоты примера мы опустим необходимость указывать количество товаров в заказе:

type Order struct {
    ID         string
    UserID     int64
    ProductIDs []int64
}

Теперь опишем структуру OrderHandler, которая будет представлять слой обработчика:

type OrderCreatorGetter interface {
    CreateOrder(order Order) (string, error)
    GetOrder(id string) (Order, error)
}

type OrderHandler struct {
    storage OrderCreatorGetter
}

Мы не стали использовать структуру OrderStorage напрямую. Мы описали интерфейс OrderCreatorGetter, который будет представлять общий интерфейс для хранилища. Это позволит в будущем заменить хранилище на другое без кода обработчика. Для этого нам достаточно будет реализовать интерфейс OrderCreatorGetter в новом хранилище.

В итоге мы получили следующее веб-приложение:

package main

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

// Запросы и ответы
type (
    CreateOrderRequest struct {
        UserID     int64   `json:"user_id"`
        ProductIDs []int64 `json:"product_ids"`
    }

    CreateOrderResponse struct {
        ID string `json:"id"`
    }

    GetOrderResponse struct {
        ID         string  `json:"id"`
        UserID     int64   `json:"user_id"`
        ProductIDs []int64 `json:"product_ids"`
    }
)

func main() {
    orderHandler := &OrderHandler{
        storage: &OrderStorage{
            orders: make(map[string]Order),
        },
    }

    webApp := fiber.New()
    webApp.Post("/orders", orderHandler.CreateOrder)
    webApp.Get("/orders/:id", orderHandler.GetOrder)

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

// Абстрактное хранилище
type OrderCreatorGetter interface {
    CreateOrder(order Order) (string, error)
    GetOrder(id string) (Order, error)
}

// Обработчик
type OrderHandler struct {
    storage OrderCreatorGetter
}

func (h *OrderHandler) CreateOrder(c *fiber.Ctx) error {
    var request CreateOrderRequest
    if err := c.BodyParser(&request); err != nil {
        return fmt.Errorf("body parser: %w", err)
    }

    order := Order{
        ID:         uuid.New().String(),
        UserID:     request.UserID,
        ProductIDs: request.ProductIDs,
    }

    id, err := h.storage.CreateOrder(order)
    if err != nil {
        return fmt.Errorf("create order: %w", err)
    }

    return c.JSON(CreateOrderResponse{
        ID: id,
    })
}

func (h *OrderHandler) GetOrder(c *fiber.Ctx) error {
    id := c.Params("id")

    order, err := h.storage.GetOrder(id)
    if err != nil {
        return fmt.Errorf("get order: %w", err)
    }

    return c.JSON(GetOrderResponse(order))
}

// Модель заказа
type Order struct {
    ID         string
    UserID     int64
    ProductIDs []int64
}

// Хранилище
type OrderStorage struct {
    orders map[string]Order
}

func (o *OrderStorage) CreateOrder(order Order) (string, error) {
    o.orders[order.ID] = order

    return order.ID, nil
}

// Ошибки
var (
    errOrderNotFound = errors.New("order not found")
)

func (o *OrderStorage) GetOrder(id string) (Order, error) {
    order, ok := o.orders[id]
    if !ok {
        return Order{}, errOrderNotFound
    }

    return order, nil
}

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

curl --location --request POST 'http://localhost/orders' \
--header 'Content-Type: application/json' \
--data-raw '{
    "user_id": 222,
    "product_ids": [1,2,3]
}'

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

curl -v 'http://localhost/orders/4b49e11e-6f59-432e-9075-3aa429fd935a'

HTTP/1.1 200 OK
{"id":"4b49e11e-6f59-432e-9075-3aa429fd935a","user_id":222,"product_ids":[1,2,3]}

Так мы создали простое веб-приложение на Go, которое хранит данные в памяти приложения. Также мы добавили интерфейс хранилища, чтобы в будущем при необходимости легко заменить хранение в памяти на хранение в любом другом хранилище.

Защита мап с мьютексом

В нашем веб-приложении на Go есть одно упущение, которое может привести к возникновению фатальной ошибки программы. Каждый HTTP-запрос обрабатывается в отдельной горутине в Go-приложениях, что может привести к ситуации, когда несколько горутин одновременно пытаются записать или читать из мапы orders. В этом случае происходит гонка данных — случай, когда две и более параллельные горутины пытаются изменить состояние одной структуры. Когда в Go-приложении возникает гонка данных с мапой, то оно завершается с фатальной ошибкой.

Чтобы избежать этой проблемы, можно использовать мьютексы. Мьютекс - это механизм синхронизации, который позволяет блокировать доступ к критической секции кода, чтобы только одна горутина могла читать или изменять данные в мапе в один момент времени.

Давайте рассмотрим простой пример использования мьютекса:

import "sync"

var (
    mu = sync.Mutex{}
    i = 0
)

func safeInc() {
    mu.Lock()
    i++
    mu.Unlock()
}

Структура мьютекс предоставляет две функции: заблокировать и разблокировать. В примере выше мы заблокировали секцию инкремента переменной i, что гарантирует изменение переменной только одной горутиной в один момент времени. Такой код безопасен для использования в производственной среде.

Мы узнали, как использовать мьютекс. Давайте перепишем хранилище OrderStorage, чтобы его можно было безопасно использовать при параллельных запросах:

type OrderStorage struct {
    mu     sync.Mutex
    orders map[string]Order
}

func (o *OrderStorage) CreateOrder(order Order) (string, error) {
    o.mu.Lock()
    defer o.mu.Unlock()

    o.orders[order.ID] = order

    return order.ID, nil
}

func (o *OrderStorage) GetOrder(id string) (Order, error) {
    o.mu.Lock()
    defer o.mu.Unlock()

    order, ok := o.orders[id]
    if !ok {
        return Order{}, errOrderNotFound
    }

    return order, nil
}

Мы добавили блокировки в каждый метод, который читает или пишет в мапу orders. Теперь только одна горутина может изменять или читать данные в мапе в один момент времени, и гонки данных больше не возникает.

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

Выводы

  • Веб-приложения используют различные хранилища для хранения данных. В самом простом случае можно хранить данные в памяти Go-приложения
  • В Go-приложениях часто используют слоеную архитектуру, когда логика обработчика отделена от логики хранилища
  • Зависимости между слоями лучше передавать через интерфейсы. Это позволит легко заменять реализацию зависимости в будущем и при этом не изменять код слоя выше
  • Если в веб-приложении в памяти используются переменные, которые могут быть изменены в разных горутинах, то всегда необходимо защищать их мьютексами

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

  1. Clean Architecture with Go

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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