From 46085e69d31365521eac698f97b2c2fd705b8232 Mon Sep 17 00:00:00 2001 From: John Berthels Date: Fri, 17 Apr 2026 14:10:30 +0100 Subject: [PATCH 1/9] add ListenWithID and StopListening --- event.go | 51 ++++++++++++++++++++++++++++++++++++++++++------ examples_test.go | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/event.go b/event.go index 72e2382..1ae814e 100644 --- a/event.go +++ b/event.go @@ -8,17 +8,23 @@ import ( // ErrEventClosed is returned when an operation is attempted on a closed event. var ErrEventClosed = errors.New("event is closed") +// ErrUnknownListener is returned when attempting to unregister an unknown id +var ErrUnknownListener = errors.New("listener id is unknown") + // Event represents a generic, thread-safe event system that can handle multiple listeners. // The type parameter T specifies the type of data that the event carries when triggered. type Event[T any] struct { - listeners []func(T) + listeners map[int]func(T) mu sync.RWMutex closed bool + next_id int } // New creates and returns a new Event instance for the specified type T. func New[T any]() *Event[T] { - return &Event[T]{} + return &Event[T]{ + listeners: make(map[int]func(T)), + } } // Trigger notifies all registered listeners by invoking their callback functions with the provided value. @@ -33,8 +39,10 @@ func (e *Event[T]) Trigger(value T) error { // Copy the listeners to avoid holding the lock during execution. // This ensures that triggering the event is thread-safe even if listeners are added or removed concurrently. - listeners := make([]func(T), len(e.listeners)) - copy(listeners, e.listeners) + var listeners []func(T) + for _, l := range e.listeners { + listeners = append(listeners, l) + } e.mu.RUnlock() var wg sync.WaitGroup @@ -56,18 +64,49 @@ func (e *Event[T]) Trigger(value T) error { // The listener will be invoked with the event's data whenever Trigger is called. // Returns ErrEventClosed if the event has been closed. func (e *Event[T]) Listen(f func(T)) error { + _, err := e.ListenWithID(f) + return err + +} + +// ListenWithID registers a new listener callback function for the event. +// The listener will be invoked with the event's data whenever Trigger is called. +// Returns ErrEventClosed if the event has been closed. +// It behaves exactly as Listen, but also returns an ID which can be used +// with StopListening to deregister the listener +func (e *Event[T]) ListenWithID(f func(T)) (int, error) { e.mu.Lock() defer e.mu.Unlock() if e.closed { - return ErrEventClosed + return -1, ErrEventClosed } - e.listeners = append(e.listeners, f) + id := e.getID() + e.listeners[id] = f + + return id, nil +} + +func (e *Event[T]) StopListening(id int) error { + e.mu.Lock() + defer e.mu.Unlock() + + _, ok := e.listeners[id] + if !ok { + return ErrUnknownListener + } + delete(e.listeners, id) return nil } +func (e *Event[T]) getID() int { + id := e.next_id + e.next_id++ + return id +} + // Close closes the event system, preventing any new listeners from being added or events from being triggered. // After calling Close, any subsequent calls to Trigger or Listen will return ErrEventClosed. // Existing listeners are removed, and resources are cleaned up. diff --git a/examples_test.go b/examples_test.go index 5c63143..aefdf87 100644 --- a/examples_test.go +++ b/examples_test.go @@ -83,3 +83,44 @@ func ExampleEvent_Close() { // 2 // 3 } + +func ExampleEvent_StopListening() { + + // Create a new event + exampleEvent := event.New[string]() + + // Listen to the event + triggerCount := 0 + listen_id, _ := exampleEvent.ListenWithID(func(v string) { + triggerCount++ + fmt.Printf("%d - %s\n", triggerCount, v) + }) + + // Trigger the event + exampleEvent.Trigger("foo") + delay() // delay for deterministic output + exampleEvent.Trigger("bar") + delay() // delay for deterministic output + exampleEvent.Trigger("baz") + + // Time for listeners to process the event + delay() + + // Stop listening + exampleEvent.StopListening(listen_id) + + // Trigger the event again + exampleEvent.Trigger("foo") + delay() // delay for deterministic output + exampleEvent.Trigger("bar") + delay() // delay for deterministic output + exampleEvent.Trigger("baz") + + // Keep the program alive + time.Sleep(time.Second) + + // Output: + // 1 - foo + // 2 - bar + // 3 - baz +} From e84d0e07365ebeb897805ad51ebec99f1b8929e0 Mon Sep 17 00:00:00 2001 From: John Berthels Date: Sat, 18 Apr 2026 08:41:15 +0100 Subject: [PATCH 2/9] review comments: avoid snake case --- event.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/event.go b/event.go index 1ae814e..8ac4f5f 100644 --- a/event.go +++ b/event.go @@ -17,7 +17,7 @@ type Event[T any] struct { listeners map[int]func(T) mu sync.RWMutex closed bool - next_id int + nextID int } // New creates and returns a new Event instance for the specified type T. @@ -102,8 +102,8 @@ func (e *Event[T]) StopListening(id int) error { } func (e *Event[T]) getID() int { - id := e.next_id - e.next_id++ + id := e.nextID + e.nextID++ return id } From 864618af06a5325ad73b8f4be12becb5b800d143 Mon Sep 17 00:00:00 2001 From: John Berthels Date: Sat, 18 Apr 2026 08:41:33 +0100 Subject: [PATCH 3/9] review comments: pre-allocate --- event.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/event.go b/event.go index 8ac4f5f..197c6cb 100644 --- a/event.go +++ b/event.go @@ -39,7 +39,7 @@ func (e *Event[T]) Trigger(value T) error { // Copy the listeners to avoid holding the lock during execution. // This ensures that triggering the event is thread-safe even if listeners are added or removed concurrently. - var listeners []func(T) + listeners := make([]func(T), 0, len(e.listeners)) for _, l := range e.listeners { listeners = append(listeners, l) } From cc834b1495b5908a3fa9bf8c9ced84836ab83fce Mon Sep 17 00:00:00 2001 From: John Berthels Date: Sat, 18 Apr 2026 08:46:17 +0100 Subject: [PATCH 4/9] review comments: lazy init of map to allow zero value use --- event.go | 10 ++++++---- event_test.go | 9 +++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/event.go b/event.go index 197c6cb..061acad 100644 --- a/event.go +++ b/event.go @@ -22,9 +22,7 @@ type Event[T any] struct { // New creates and returns a new Event instance for the specified type T. func New[T any]() *Event[T] { - return &Event[T]{ - listeners: make(map[int]func(T)), - } + return &Event[T]{} } // Trigger notifies all registered listeners by invoking their callback functions with the provided value. @@ -66,7 +64,6 @@ func (e *Event[T]) Trigger(value T) error { func (e *Event[T]) Listen(f func(T)) error { _, err := e.ListenWithID(f) return err - } // ListenWithID registers a new listener callback function for the event. @@ -78,6 +75,11 @@ func (e *Event[T]) ListenWithID(f func(T)) (int, error) { e.mu.Lock() defer e.mu.Unlock() + // Lazy init to allow use of zero value + if e.listeners == nil { + e.listeners = make(map[int]func(T)) + } + if e.closed { return -1, ErrEventClosed } diff --git a/event_test.go b/event_test.go index 0e4b82e..ddb03e8 100644 --- a/event_test.go +++ b/event_test.go @@ -1 +1,10 @@ package event + +import "testing" + +func TestZeroValue(t *testing.T) { + var ev Event[string] + + // Can listen to zero value without panicing + ev.Listen(func(v string) { println(v) }) +} From ccd7edeb50fb590c8dfc1c2a627ce640a508929f Mon Sep 17 00:00:00 2001 From: John Berthels Date: Sat, 18 Apr 2026 08:57:17 +0100 Subject: [PATCH 5/9] review comments: add missing doc comment --- event.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/event.go b/event.go index 061acad..22a2afc 100644 --- a/event.go +++ b/event.go @@ -91,6 +91,9 @@ func (e *Event[T]) ListenWithID(f func(T)) (int, error) { return id, nil } +// StopListening unregisters a listener, using the ID returned from Listen. +// The callback which was registered with that ID will no longer be called +// and any associated resources will be released. func (e *Event[T]) StopListening(id int) error { e.mu.Lock() defer e.mu.Unlock() From 1d4f816f32aa23c21aabb71e1b32ca2ec2c90343 Mon Sep 17 00:00:00 2001 From: John Berthels Date: Sat, 18 Apr 2026 08:57:41 +0100 Subject: [PATCH 6/9] review comments: remove blank line --- examples_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/examples_test.go b/examples_test.go index aefdf87..705472a 100644 --- a/examples_test.go +++ b/examples_test.go @@ -85,7 +85,6 @@ func ExampleEvent_Close() { } func ExampleEvent_StopListening() { - // Create a new event exampleEvent := event.New[string]() From 0d33a224cfc05fc791abaa7fc54cef6c350f322b Mon Sep 17 00:00:00 2001 From: John Berthels Date: Sat, 18 Apr 2026 08:58:02 +0100 Subject: [PATCH 7/9] review comments: snake case --- examples_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples_test.go b/examples_test.go index 705472a..4a1a4c7 100644 --- a/examples_test.go +++ b/examples_test.go @@ -90,7 +90,7 @@ func ExampleEvent_StopListening() { // Listen to the event triggerCount := 0 - listen_id, _ := exampleEvent.ListenWithID(func(v string) { + listenID, _ := exampleEvent.ListenWithID(func(v string) { triggerCount++ fmt.Printf("%d - %s\n", triggerCount, v) }) @@ -106,7 +106,7 @@ func ExampleEvent_StopListening() { delay() // Stop listening - exampleEvent.StopListening(listen_id) + exampleEvent.StopListening(listenID) // Trigger the event again exampleEvent.Trigger("foo") From fd1d9d3e181bba74fa398a406268302675ce3434 Mon Sep 17 00:00:00 2001 From: John Berthels Date: Sat, 18 Apr 2026 08:58:26 +0100 Subject: [PATCH 8/9] review comments: full stop --- event.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/event.go b/event.go index 22a2afc..e59b384 100644 --- a/event.go +++ b/event.go @@ -8,7 +8,7 @@ import ( // ErrEventClosed is returned when an operation is attempted on a closed event. var ErrEventClosed = errors.New("event is closed") -// ErrUnknownListener is returned when attempting to unregister an unknown id +// ErrUnknownListener is returned when attempting to unregister an unknown id. var ErrUnknownListener = errors.New("listener id is unknown") // Event represents a generic, thread-safe event system that can handle multiple listeners. From 7750aabfc7df81c4ae3e280e4952559b187d50a7 Mon Sep 17 00:00:00 2001 From: John Berthels Date: Sat, 18 Apr 2026 09:02:00 +0100 Subject: [PATCH 9/9] drop old Listen, breaking API change is acceptable --- event.go | 14 +++----------- examples_test.go | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/event.go b/event.go index e59b384..9c4592a 100644 --- a/event.go +++ b/event.go @@ -60,18 +60,9 @@ func (e *Event[T]) Trigger(value T) error { // Listen registers a new listener callback function for the event. // The listener will be invoked with the event's data whenever Trigger is called. +// Returns an ID which can be used with StopListening to deregister the listener. // Returns ErrEventClosed if the event has been closed. -func (e *Event[T]) Listen(f func(T)) error { - _, err := e.ListenWithID(f) - return err -} - -// ListenWithID registers a new listener callback function for the event. -// The listener will be invoked with the event's data whenever Trigger is called. -// Returns ErrEventClosed if the event has been closed. -// It behaves exactly as Listen, but also returns an ID which can be used -// with StopListening to deregister the listener -func (e *Event[T]) ListenWithID(f func(T)) (int, error) { +func (e *Event[T]) Listen(f func(T)) (int, error) { e.mu.Lock() defer e.mu.Unlock() @@ -94,6 +85,7 @@ func (e *Event[T]) ListenWithID(f func(T)) (int, error) { // StopListening unregisters a listener, using the ID returned from Listen. // The callback which was registered with that ID will no longer be called // and any associated resources will be released. +// If the ID passed in is not registered, ErrUnknownListener will be returned. func (e *Event[T]) StopListening(id int) error { e.mu.Lock() defer e.mu.Unlock() diff --git a/examples_test.go b/examples_test.go index 4a1a4c7..6f93151 100644 --- a/examples_test.go +++ b/examples_test.go @@ -90,7 +90,7 @@ func ExampleEvent_StopListening() { // Listen to the event triggerCount := 0 - listenID, _ := exampleEvent.ListenWithID(func(v string) { + listenID, _ := exampleEvent.Listen(func(v string) { triggerCount++ fmt.Printf("%d - %s\n", triggerCount, v) })