Event Bus и паттерн Observer в Go (Golang): реализация pub/sub

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Event Bus и паттерн Observer в Go (Golang): от теории к практике

Паттерн Observer (наблюдатель) и его реализация в виде Event Bus (шины событий) — фундаментальные инструменты для построения слабосвязанных, масштабируемых систем. В Go, с его философией «общайтесь через каналы, а не через разделяемую память», эти паттерны приобретают особую элегантность. В этой статье мы разберем, как создать собственный Event Bus на Go, используя горутины и каналы, и рассмотрим типовые сценарии использования.

Паттерн Observer: суть и проблема

Представьте, что у вас есть система, где один объект (издатель) должен уведомлять множество других (подписчиков) об изменении своего состояния. Без Observer вам пришлось бы жестко связывать издателя с каждым подписчиком. Observer решает это, вводя интерфейс подписки: издатель хранит список подписчиков и вызывает у них метод Update() при наступлении события.

Event Bus — это централизованная шина, которая берет на себя роль посредника. Любой компонент может опубликовать событие, а любой другой — подписаться на него, не зная о существовании друг друга.

Почему Go идеален для Event Bus?

  • Горутины и каналы: асинхронная обработка событий без блокировок.
  • Интерфейсы: позволяют легко определять контракты для обработчиков.
  • Отсутствие наследования: композиция через каналы и функции естественнее, чем классическая ООП-реализация.

Простая реализация Event Bus на Go

Начнем с базовой версии, где события — это строки, а подписчики — функции-обработчики.

package main

import (
    "fmt"
    "sync"
    "time"
)

// EventBus — наша шина событий
type EventBus struct {
    subscribers map[string][]func(string)
    mu          sync.RWMutex
}

// NewEventBus создает новую шину
func NewEventBus() *EventBus {
    return &EventBus{
        subscribers: make(map[string][]func(string)),
    }
}

// Subscribe подписывает обработчик на событие
func (eb *EventBus) Subscribe(event string, handler func(string)) {
    eb.mu.Lock()
    defer eb.mu.Unlock()
    eb.subscribers[event] = append(eb.subscribers[event], handler)
}

// Publish публикует событие и вызывает всех подписчиков синхронно
func (eb *EventBus) Publish(event string, data string) {
    eb.mu.RLock()
    defer eb.mu.RUnlock()
    if handlers, ok := eb.subscribers[event]; ok {
        for _, handler := range handlers {
            handler(data)
        }
    }
}

func main() {
    bus := NewEventBus()
    
    bus.Subscribe("user.login", func(data string) {
        fmt.Printf("Логин: %s\n", data)
    })
    
    bus.Subscribe("user.login", func(data string) {
        fmt.Printf("Аудит: %s вошел в систему\n", data)
    })
    
    bus.Publish("user.login", "alice@example.com")
    // Вывод:
    // Логин: alice@example.com
    // Аудит: alice@example.com вошел в систему
}

Этот код работает, но есть нюанс: обработчики вызываются синхронно. Если один обработчик зависнет, он заблокирует всю шину.

Асинхронная версия с каналами

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

type AsyncEventBus struct {
    subscribers map[string][]chan string
    mu          sync.RWMutex
}

func NewAsyncEventBus() *AsyncEventBus {
    return &AsyncEventBus{
        subscribers: make(map[string][]chan string),
    }
}

func (eb *AsyncEventBus) Subscribe(event string, buffer int) <-chan string {
    eb.mu.Lock()
    defer eb.mu.Unlock()
    ch := make(chan string, buffer)
    eb.subscribers[event] = append(eb.subscribers[event], ch)
    return ch
}

func (eb *AsyncEventBus) Publish(event string, data string) {
    eb.mu.RLock()
    defer eb.mu.RUnlock()
    if channels, ok := eb.subscribers[event]; ok {
        for _, ch := range channels {
            select {
            case ch <- data:
            default:
                // Если канал полон, пропускаем (non-blocking)
            }
        }
    }
}

// Использование
func main() {
    bus := NewAsyncEventBus()
    ch := bus.Subscribe("order.created", 10)
    
    go func() {
        for data := range ch {
            fmt.Println("Получен заказ:", data)
            time.Sleep(100 * time.Millisecond) // имитация работы
        }
    }()
    
    bus.Publish("order.created", "Заказ #123")
    time.Sleep(200 * time.Millisecond)
}

Продвинутая версия: отписка и контекст

Рекомендаций