Как реализовать механизм событий или паттерн ‘Наблюдатель’ (Observer) в Go?

Ответ

В Go нет встроенной поддержки триггеров как в SQL, но их можно эмулировать с помощью методов-наблюдателей или паттерна "Наблюдатель" (Observer), который позволяет объектам уведомлять другие объекты об изменениях своего состояния.

Пример (базовая реализация паттерна Observer):

package main

import "fmt"

// Trigger определяет тип функции-наблюдателя, которая будет вызываться при изменении.
// Она принимает старое и новое значение.
type Trigger func(old, new interface{})

// Observable представляет объект, за которым можно наблюдать.
type Observable struct {
    value    interface{}
    triggers []Trigger
}

// Set устанавливает новое значение и уведомляет всех наблюдателей.
func (o *Observable) Set(value interface{}) {
    old := o.value
    o.value = value
    // Вызываем все зарегистрированные триггеры
    for _, trigger := range o.triggers {
        trigger(old, value)
    }
}

// AddTrigger добавляет новую функцию-наблюдатель к списку.
func (o *Observable) AddTrigger(t Trigger) {
    o.triggers = append(o.triggers, t)
}

func main() {
    obj := Observable{}

    // Добавляем первый наблюдатель
    obj.AddTrigger(func(old, new interface{}) {
        fmt.Printf("Наблюдатель 1: Значение изменилось с %v на %vn", old, new)
    })

    // Добавляем второй наблюдатель
    obj.AddTrigger(func(old, new interface{}) {
        fmt.Printf("Наблюдатель 2: Обновление! Новое значение: %vn", new)
    })

    fmt.Println("Устанавливаем значение 42...")
    obj.Set(42) // Вызовет оба триггера

    fmt.Println("nУстанавливаем значение 'hello'...")
    obj.Set("hello") // Вызовет оба триггера

    fmt.Println("nУстанавливаем значение 123.45...")
    obj.Set(123.45) // Вызовет оба триггера
}

Пояснения и улучшения:

  • Эта базовая реализация использует interface{}, что позволяет передавать любые типы данных. Однако это требует ручной проверки типов (с помощью утверждений типа value.(int)) при их использовании внутри функций Trigger.
  • В Go 1.18+ для более типобезопасных решений можно использовать дженерики (generics), чтобы определить Observable и Trigger для конкретного типа данных, избегая необходимости в interface{} и утверждениях типа.
  • Для более сложных сценариев, таких как асинхронная обработка событий, отписка от событий, или когда требуется более сложная маршрутизация событий, можно использовать:
    • Event Bus: Специализированные библиотеки, такие как github.com/asaskevich/EventBus, предоставляют более мощные механизмы для публикации и подписки на события.
    • Системы очередей сообщений: Для распределенных систем или когда требуется надежная доставка и персистентность событий, используются внешние брокеры сообщений, такие как Kafka, RabbitMQ или NATS.