Привет, Хабр!
Сегодня мы попробуем реализовать управление состоянием в 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 ноября: «Паттерны отказоустойчивости и масштабируемости микросервисной архитектуры». Узнать подробнее
RodionGork
Это наверное полезно и интересно, но сам паттерн у вас описан как-то очень куце - из-за чего не зная о чем речь довольно трудно понять что вы собрались сделать и неужели без паттерна этот функционал будет выглядеть сильно иначе. Может быть стоит расширить этот раздел.
В то же время если рассказывать про паттерн, стоит ли его прибивать кодом к конкретному языку?
ну всё же удаление элемента можно экономичнее сделать - просто последний перемещаете на место удалённого и уменьшаете длину...
но главное - если вам их надо мэпить по айдишникам, чего ж мэпу-то не использовать?