Golang + Redux

от автора

Привет, Хабр!

Сегодня мы попробуем реализовать управление состоянием в Go‑приложениях с помощью паттерна Redux. Да‑да, Redux не только для JS.

Redux — это предсказуемый контейнер состояния для приложений. Он помогает управлять состоянием приложения централизованно, делая его более предсказуемым и удобным для отладки. В основном Redux ассоциируется с фронтендом на JavaScript, но принципы, лежащие в его основе, иногда могут подойти и для Go‑приложений.

Основные концепции Redux:

  1. Store: Централизованное хранилище состояния.

  2. Actions: Описания того, что произошло.

  3. Reducers: Функции, которые определяют, как состояние изменяется в ответ на действия.

  4. Dispatch: Процесс отправки действий в хранилище.

Создаем Redux-подобную систему в Go

Определяем состояние

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

// state.go package main  // Cat представляет собой котика type Cat struct {     ID        int     Name      string     Breed     string     IsAdopted bool }  // AppState хранит текущее состояние приложения type AppState struct {     Cats []Cat }

Определяем действия

Действия описывают, что происходит в нашем приложении. Например, добавление задачи, удаление задачи или изменение статуса задачи.

// actions.go package main  // ActionType определяет тип действия type ActionType string  const (     AddCat          ActionType = "ADD_CAT"     RemoveCat       ActionType = "REMOVE_CAT"     ToggleAdoption  ActionType = "TOGGLE_ADOPTION" )  // Action представляет собой действие type Action struct {     Type    ActionType     Payload interface{} }

Создаем редьюсеры

Редьюсеры определяют, как состояние изменяется в ответ на действия.

// reducers.go package main  // Reducer функция, которая принимает состояние и действие, и возвращает новое состояние type Reducer func(state AppState, action Action) AppState  // rootReducer объединяет все редьюсеры func rootReducer(state AppState, action Action) AppState {     switch action.Type {     case AddCat:         cat, ok := action.Payload.(Cat)         if !ok {             return state         }         cat.ID = len(state.Cats) + 1         state.Cats = append(state.Cats, cat)     case RemoveCat:         id, ok := action.Payload.(int)         if !ok {             return state         }         for i, cat := range state.Cats {             if cat.ID == id {                 state.Cats = append(state.Cats[:i], state.Cats[i+1:]...)                 break             }         }     case ToggleAdoption:         id, ok := action.Payload.(int)         if !ok {             return state         }         for i, cat := range state.Cats {             if cat.ID == id {                 state.Cats[i].IsAdopted = !state.Cats[i].IsAdopted                 break             }         }     default:         // Неизвестное действие, возвращаем состояние без изменений     }     return state }

Создаем хранилище

Хранилище управляет состоянием и обрабатывает действия через редьюсеры.

// store.go package main  import "sync"  // Store хранит состояние и позволяет подписываться на его изменения type Store struct {     state       AppState     reducer     Reducer     mutex       sync.RWMutex     subscribers []chan AppState }  // NewStore создает новое хранилище func NewStore(reducer Reducer, initialState AppState) *Store {     return &Store{         state:       initialState,         reducer:     reducer,         subscribers: make([]chan AppState, 0),     } }  // GetState возвращает текущее состояние func (s *Store) GetState() AppState {     s.mutex.RLock()     defer s.mutex.RUnlock()     return s.state }  // Dispatch отправляет действие и обновляет состояние func (s *Store) Dispatch(action Action) {     s.mutex.Lock()     s.state = s.reducer(s.state, action)     // Копируем подписчиков, чтобы избежать блокировок     subscribers := append([]chan AppState{}, s.subscribers...)     s.mutex.Unlock()      // Уведомляем всех подписчиков     for _, sub := range subscribers {         // Не блокируем основной поток         go func(ch chan AppState) {             ch <- s.state         }(sub)     } }  // Subscribe добавляет нового подписчика func (s *Store) Subscribe() chan AppState {     s.mutex.Lock()     defer s.mutex.Unlock()     ch := make(chan AppState, 1)     s.subscribers = append(s.subscribers, ch)     // Отправляем текущее состояние сразу после подписки     ch <- s.state     return ch }

Используем хранилище в приложении

Теперь можно использовать хранилище, редьюсеры и действия, приложении:

// main.go package main  import (     "fmt"     "time" )  func main() {     initialState := AppState{         Cats: []Cat{},     }      store := NewStore(rootReducer, initialState)      // Подписываемся на изменения состояния     subscriber := store.Subscribe()      // Запускаем горутину для обработки изменений состояния     go func() {         for state := range subscriber {             fmt.Println("Текущее состояние котиков:")             for _, cat := range state.Cats {                 status := "Не усыновлен"                 if cat.IsAdopted {                     status = "Усыновлен"                 }                 fmt.Printf("ID: %d, Имя: %s, Порода: %s, Статус: %s\n", cat.ID, cat.Name, cat.Breed, status)             }             fmt.Println("-----")         }     }()      // Диспатчим действия     store.Dispatch(Action{         Type: AddCat,         Payload: Cat{             Name:  "Мурзик",             Breed: "Сиамская",         },     })      store.Dispatch(Action{         Type: AddCat,         Payload: Cat{             Name:  "Барсик",             Breed: "Британская",         },     })      time.Sleep(500 * time.Millisecond) // Ждем, чтобы горутина успела обработать      store.Dispatch(Action{         Type:    ToggleAdoption,         Payload: 1,     })      time.Sleep(500 * time.Millisecond) // Ждем обновлений      store.Dispatch(Action{         Type:    RemoveCat,         Payload: 2,     })      time.Sleep(500 * time.Millisecond) // Ждем финальных обновлений }

Запускаем

После запуска получим следующий вывод:

Текущее состояние котиков: ID: 1, Имя: Мурзик, Порода: Сиамская, Статус: Не усыновлен ----- Текущее состояние котиков: ID: 1, Имя: Мурзик, Порода: Сиамская, Статус: Не усыновлен ID: 2, Имя: Барсик, Порода: Британская, Статус: Не усыновлен ----- Текущее состояние котиков: ID: 1, Имя: Мурзик, Порода: Сиамская, Статус: Усыновлен ID: 2, Имя: Барсик, Порода: Британская, Статус: Не усыновлен ----- Текущее состояние котиков: ID: 1, Имя: Мурзик, Порода: Сиамская, Статус: Усыновлен -----

Итак, этот паттерн хорошо впишется, если нужно централизованно управлять состоянием приложения и следить за его изменениями. Если проект растет и управление состоянием становится сложным, Redux-подход в Go может в чем-то упростить жизнь.

Но есть и пару моментов, о которых стоит помнить:

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

  • Будь осторожен с подписками — без механизма отписки могут возникнуть утечки горутин.

  • Не перегружай хранилище: хранение слишком большого состояния может замедлить приложение.

  • Избегай гонок данных — всегда используй мьютексы или другие механизмы синхронизации при работе с состоянием.

Используй Redux-подход разумно. Если есть вопросы или идеи, пишите в комментариях.

В рамках курса «Software Architect» в ноябре пройдут открытые уроки:

  • 7 ноября: «Стратегии тестирования в архитектуре микросервисов». Узнать подробнее

  • 19 ноября: «Паттерны отказоустойчивости и масштабируемости микросервисной архитектуры». Узнать подробнее


ссылка на оригинал статьи https://habr.com/ru/articles/854306/