Event Bus and Observer Pattern in Go (Golang)
In the world of modern Go (Golang) development, there is often a need to organize loosely coupled interaction between application components. Two classic approaches are the Observer pattern and its more advanced implementation — the Event Bus. These mechanisms allow some parts of a program to react to actions of others without creating rigid dependencies.
In this article, we will break down how to implement an Event Bus in pure Go without external dependencies, examine the Observer pattern, and show code examples for synchronous and asynchronous event handling.
What is the Observer Pattern?
The Observer pattern is a behavioral design pattern that creates a subscription mechanism allowing one object to watch and react to events occurring in other objects.
Main elements:
- Subject — the object that generates events.
- Observer — the object that subscribes to events.
- Event — the data transmitted from the subject to observers.
What is an Event Bus?
An Event Bus is a centralized bus that manages the transmission of events between publishers and subscribers. Unlike the classic Observer pattern, where the subject directly stores a list of observers, the Event Bus acts as an intermediary. This makes the system even more flexible and scalable.
Implementing an Event Bus in Go
Go does not have a built-in Event Bus implementation, but it can be easily written using goroutines, channels, and interfaces. Let's look at a simple but complete implementation.
1. Define basic types
// Event represents an event structuretype Event struct { Name string Data interface{}}
// EventHandler is an event handler functiontype EventHandler func(Event)2. Create the Event Bus
type EventBus struct { handlers map[string][]EventHandler mu sync.RWMutex}
// NewEventBus creates a new event busfunc NewEventBus() *EventBus { return &EventBus{ handlers: make(map[string][]EventHandler), }}
// Subscribe subscribes a handler to an eventfunc (eb *EventBus) Subscribe(name string, handler EventHandler) { eb.mu.Lock() defer eb.mu.Unlock() eb.handlers[name] = append(eb.handlers[name], handler)}
// Publish synchronously publishes an eventfunc (eb *EventBus) Publish(name string, data interface{}) { eb.mu.RLock() defer eb.mu.RUnlock() if handlers, ok := eb.handlers[name]; ok { event := Event{Name: name, Data: data} for _, handler := range handlers { handler(event) } }}3. Asynchronous publishing
For asynchronous processing, you can run each handler in a separate goroutine:
// PublishAsync asynchronously publishes an eventfunc (eb *EventBus) PublishAsync(name string, data interface{}) { eb.mu.RLock() defer eb.mu.RUnlock() if handlers, ok := eb.handlers[name]; ok { event := Event{Name: name, Data: data} for _, handler := range handlers { go handler(event) } }}Usage Example
Let's look at how to use our Event Bus in a real application. Suppose we have a system that logs user actions and sends notifications.
func main() { bus := NewEventBus()
// Subscribe to the "user.login" event bus.Subscribe("user.login", func(e Event) { userData := e.Data.(map[string]string) fmt.Printf("Log: User %s logged in", userData["username"]) })
// Subscribe to the same event for