In this blog post, we will explore how to implement an event bus using the Go programming language (Golang) and take advantage of the generics feature introduced in Go 1.18 to make the event bus more flexible and type-safe. Event buses are a fundamental component in event-driven architectures, enabling efficient communication between different parts of your application. By the end of this post, you'll have a solid understanding of how to create a generic event bus in Go.

What is an Event Bus?

An event bus is a publish-subscribe pattern that allows different parts of your application to communicate with each other without needing direct references or dependencies. It consists of three main components: publishers, subscribers, and events.

  • Publishers: These are responsible for generating events and broadcasting them to the event bus.

  • Subscribers: These are entities that listen for specific types of events and perform actions when those events occur.

  • Events: These are data structures that carry information about what has happened. They are sent from publishers to subscribers.

Creating the Event Bus

Let's start by defining the core structure of our event bus. We'll use generics to create a flexible event bus that can handle different types of events.

package main

import (
    "sync"
)

// EventBus represents a generic event bus.
type EventBus[T any] struct {
    subscribers map[EventType]map[Subscriber]struct{}
    mutex       sync.Mutex
}

// EventType is the type representing different event types.
type EventType string

// Subscriber is a function that handles events.
type Subscriber func(event T)

// NewEventBus creates a new event bus.
func NewEventBus[T any]() *EventBus[T] {
    return &EventBus[T]{
        subscribers: make(map[EventType]map[Subscriber]struct{}),
    }
}

// Subscribe adds a subscriber for a specific event type.
func (eb *EventBus[T]) Subscribe(eventType EventType, subscriber Subscriber) {
    eb.mutex.Lock()
    defer eb.mutex.Unlock()

    if eb.subscribers[eventType] == nil {
        eb.subscribers[eventType] = make(map[Subscriber]struct{})
    }

    eb.subscribers[eventType][subscriber] = struct{}{}
}

// Unsubscribe removes a subscriber for a specific event type.
func (eb *EventBus[T]) Unsubscribe(eventType EventType, subscriber Subscriber) {
    eb.mutex.Lock()
    defer eb.mutex.Unlock()

    if subscribers, ok := eb.subscribers[eventType]; ok {
        delete(subscribers, subscriber)

        // Cleanup the event type map if there are no subscribers left.
        if len(subscribers) == 0 {
            delete(eb.subscribers, eventType)
        }
    }
}

// Publish sends an event to all subscribers of a specific event type.
func (eb *EventBus[T]) Publish(eventType EventType, event T) {
    eb.mutex.Lock()
    defer eb.mutex.Unlock()

    if subscribers, ok := eb.subscribers[eventType]; ok {
        for subscriber := range subscribers {
            subscriber(event)
        }
    }
}

Using the Event Bus

Now that we have our generic event bus defined, let's see how we can use it in our application.

package main

import (
    "fmt"
    "time"
)

func main() {
    // Create a new event bus for string events.
    eventBus := NewEventBus[string]()

    // Define a subscriber function for string events.
    stringSubscriber := func(event string) {
        fmt.Printf("Received string event: %s\n", event)
    }

    // Subscribe to a specific event type.
    eventBus.Subscribe("stringEvent", stringSubscriber)

    // Publish a string event.
    eventBus.Publish("stringEvent", "Hello, Event Bus!")

    // Unsubscribe the subscriber.
    eventBus.Unsubscribe("stringEvent", stringSubscriber)

    // The subscriber will not receive events after unsubscribing.
    eventBus.Publish("stringEvent", "This event won't be received.")

    // Sleep to allow time for the event bus to finish processing.
    time.Sleep(time.Second)
}

In this example, we create an event bus for string events, subscribe to a specific event type ("stringEvent"), publish an event, and then unsubscribe the subscriber. After unsubscribing, the subscriber won't receive any more events of that type.

Conclusion

Implementing an event bus in Go using generics allows you to create a flexible and type-safe communication channel within your application. This can be particularly useful in complex applications where different parts need to communicate without tight coupling. By leveraging the power of generics in Go 1.18, you can create a reusable event bus that works seamlessly with various event types while ensuring type safety.

go-eventbus

If you prefer to use a library instead of implementing your own event bus, go-eventbus is a good choice.