Golang + Redux

Kate

Administrator
Команда форума
Сегодня мы попробуем реализовать управление состоянием в 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.IsAdopted = !state.Cats.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 может в чем-то упростить жизнь.

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

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

 
Сверху