Привет, Хабр!
Сегодня мы попробуем реализовать управление состоянием в Go‑приложениях с помощью паттерна Redux. Да‑да, Redux не только для JS.
Redux — это предсказуемый контейнер состояния для приложений. Он помогает управлять состоянием приложения централизованно, делая его более предсказуемым и удобным для отладки. В основном Redux ассоциируется с фронтендом на JavaScript, но принципы, лежащие в его основе, иногда могут подойти и для Go‑приложений.
Основные концепции Redux:
-
Store: Централизованное хранилище состояния.
-
Actions: Описания того, что произошло.
-
Reducers: Функции, которые определяют, как состояние изменяется в ответ на действия.
-
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/
Добавить комментарий