The task requires building a simple event emitter in Go. This implementation provides a type-safe, concurrent-safe event system with listener management and event dispatching capabilities.
package main
import (
"fmt"
"sync"
)
// EventType represents a uniquely identifiable event type using a custom string type for type safety
type EventType string
// ListenerFunc defines the signature for event listener callbacks with error handling capabilities
type ListenerFunc func(data interface{}) error
// EventEmitterConfig holds configuration parameters for the emitter (extensibility pattern)
type EventEmitterConfig struct {
MaxListeners int
BufferSize int
}
// EventEmitter is a thread-safe event emitter using generics and concurrent patterns
type EventEmitter struct {
listeners map[EventType][]ListenerFunc
mu sync.RWMutex
config EventEmitterConfig
wg sync.WaitGroup
}
// NewEventEmitter creates a new instance of EventEmitter with sensible defaults
func NewEventEmitter(config *EventEmitterConfig) *EventEmitter {
if config == nil {
config = &EventEmitterConfig{
MaxListeners: 10,
BufferSize: 100,
}
}
return &EventEmitter{
listeners: make(map[EventType][]ListenerFunc),
config: *config,
}
}
// On registers a listener for a specific event type (observer pattern implementation)
func (e *EventEmitter) On(eventType EventType, listener ListenerFunc) error {
e.mu.Lock()
defer e.mu.Unlock()
if len(e.listeners[eventType]) >= e.config.MaxListeners {
return fmt.Errorf("max listeners exceeded for event type: %s", eventType)
}
e.listeners[eventType] = append(e.listeners[eventType], listener)
return nil
}
// Emit dispatches an event to all registered listeners with concurrent execution
func (e *EventEmitter) Emit(eventType EventType, data interface{}) error {
e.mu.RLock()
listenersCopy := make([]ListenerFunc, len(e.listeners[eventType]))
copy(listenersCopy, e.listeners[eventType])
e.mu.RUnlock()
for _, listener := range listenersCopy {
e.wg.Add(1)
go func(fn ListenerFunc) {
defer e.wg.Done()
if err := fn(data); err != nil {
fmt.Printf("listener error: %vn", err)
}
}(listener)
}
e.wg.Wait()
return nil
}
// RemoveListener removes a specific listener from an event type (cleanup operation)
func (e *EventEmitter) RemoveListener(eventType EventType, index int) error {
e.mu.Lock()
defer e.mu.Unlock()
if index < 0 || index >= len(e.listeners[eventType]) {
return fmt.Errorf("invalid listener index")
}
e.listeners[eventType] = append(e.listeners[eventType][:index], e.listeners[eventType][index+1:]...)
return nil
}
func main() {
emitter := NewEventEmitter(nil)
emitter.On("user_login", func(data interface{}) error {
fmt.Println("User logged in:", data)
return nil
})
emitter.Emit("user_login", "alice@example.com")
}
Code Review
1. Lines 15-18. EventEmitterConfig struct with MaxListeners and BufferSize fields, but BufferSize is defined and never used anywhere in the code. This looks like cargo cult configuration that someone thought might be useful later.
2. Line 26. The WaitGroup is initialized here but there's a logical issue – we're calling wg.Wait() inside Emit() which means every Emit call blocks until all goroutines finish. This defeats the entire purpose of spawning them as goroutines in the first place.
3. Lines 40-44. NewEventEmitter accepts a pointer to EventEmitterConfig and then dereferences it into the struct. Either take a value or return a pointer to avoid this awkward nil checking pattern. Also, the 'sensible defaults' comment is doing a lot of work here.
4. Lines 52-55. The MaxListeners check is overly restrictive and will prevent adding legitimate listeners. In a real event system you'd either remove this entirely or make it a warning, not a hard error. This breaks basic functionality for no real benefit.
5. Lines 62-65. Making a copy of the listeners slice is good practice for thread safety, but then we're spawning goroutines that capture the listener in a loop variable closure. The defer in the anonymous function is also redundant given the function is so simple.
6. Lines 73-80. RemoveListener takes an integer index. This is an awkward API. How do callers know which index they registered their listener at? This should probably take the listener function itself or return a token from On() that can be used to unsubscribe.
7. Lines 1-10. The whole thing looks over-engineered for 'a simple event emitter.' The original ask probably wanted a basic map and mutex, but we've added config structs, error handling for every edge case, and concurrent execution that might not be desired. Keep it simple.