Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 42 additions & 6 deletions event.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ 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
nextID int
}
Comment on lines 16 to 21
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

next_id uses snake_case. This is not idiomatic Go and is likely to be reported by stylecheck (ST1003). Rename to nextID (and update getID accordingly).

Copilot uses AI. Check for mistakes.

// New creates and returns a new Event instance for the specified type T.
Expand All @@ -33,8 +37,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)
listeners := make([]func(T), 0, len(e.listeners))
for _, l := range e.listeners {
listeners = append(listeners, l)
}
e.mu.RUnlock()

var wg sync.WaitGroup
Expand All @@ -54,20 +60,50 @@ 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 {
func (e *Event[T]) Listen(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 ErrEventClosed
return -1, ErrEventClosed
}

e.listeners = append(e.listeners, f)
id := e.getID()

e.listeners[id] = f

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.
// 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()

_, ok := e.listeners[id]
if !ok {
return ErrUnknownListener
}
delete(e.listeners, id)
return nil
}
Comment on lines +89 to 99
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StopListening is an exported method but it lacks a doc comment. stylecheck typically requires exported identifiers to have a comment that starts with the identifier name. Please add a // StopListening ... comment describing behavior (including what happens when the event is closed or the ID is unknown).

Copilot uses AI. Check for mistakes.

func (e *Event[T]) getID() int {
id := e.nextID
e.nextID++
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.
Expand Down
9 changes: 9 additions & 0 deletions event_test.go
Original file line number Diff line number Diff line change
@@ -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) })
}
40 changes: 40 additions & 0 deletions examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,43 @@ func ExampleEvent_Close() {
// 2
// 3
}

func ExampleEvent_StopListening() {
// Create a new event
exampleEvent := event.New[string]()

// Listen to the event
triggerCount := 0
listenID, _ := exampleEvent.Listen(func(v string) {
triggerCount++
fmt.Printf("%d - %s\n", triggerCount, v)
Comment on lines +92 to +95
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

listen_id uses snake_case, which is not idiomatic Go and may be flagged by stylecheck (ST1003) even in _test.go files. Rename to listenID for consistency with the rest of the examples.

Copilot uses AI. Check for mistakes.
})

// 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(listenID)

// 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
}