IN-PROCESS · FIRE-AND-FORGET · v1.2.0
An event bus that moves at memory.
A minimal in-process event bus for Go — typed events, cancelable listeners, one-shot handlers. Built on the zero-allocation go-pubsub engine. The emit hot path allocates nothing for a known event; a brand-new event value costs one topic slot.
Why go-bus
Small surface. Sharp edges.
- Typed events — dispatch
anypayloads keyed by a typedEvent. - One-shot handlers — listeners that auto-remove after their first trigger.
- Context cancel — every listener owns a
context.Context. - Panic-safe — a panicking listener is recovered and logged; the bus survives.
- Thin layer — a focused wrapper over go-pubsub, no hidden runtime.
When
Right tool, narrow job.
A great fit
- Domain & application events fanned out in one process
- Decoupling producers from consumers
- One-shot hooks — “run once on first X”
- Lightweight fan-out to N subscribers
Not a good fit
- Cross-process / distributed pub-sub — use NATS, Kafka
- Durable queues or at-least-once delivery
- Strict reliability — full channels drop silently
- Backpressure / acknowledgement paths
go-bus is fire-and-forget by design: Emit never blocks, there is no
persistence, no redelivery. If a subscriber falls behind, messages are dropped.
Start
Five steps to first event.
// 1 · create a bus (inject your own logger if you like)
eventBus, _ := bus.NewBus(bus.WithLogger(logger))
// 2 · events are typed constants
const ( ConfigReloaded = iota + 1; ThresholdBreached )
reload := bus.NewEvent(ConfigReloaded)
// 3 · register a listener
listener := bus.NewListener(func(msg any) { invalidate(msg) })
_ = eventBus.On(reload, listener)
// 4 · emit — non-blocking, zero-alloc
_ = eventBus.Emit(reload, "new.yaml")
// 5 · stop listening whenever
listener.Cancel()
Or grab it: go get github.com/F2077/go-bus · requires Go 1.25+.
A runnable copy lives at cmd/quickstart.
Surface
The whole API, one screen.
bus.NewBus(opts...) → (*Bus, error)Construct a bus. WithLogger injects an slog.Logger.
bus.NewEvent(int) → EventWrap an integer constant into a typed event key.
(*Bus).On(Event, *Listener) → errorSubscribe a listener; one goroutine drains it until canceled.
(*Bus).Emit(Event, any) → errorDispatch to every listener on that event. Never blocks.
bus.NewListener(ListenFunc, opts...) → *ListenerWithOnetime(true) auto-removes after the first trigger.
(*Listener).Cancel()Stop the listener’s goroutine via its context.
Numbers
Measured, not promised.
| Benchmark | Setup | ns/op | B/op | allocs |
|---|---|---|---|---|
BenchmarkEventBus | 1000 listeners, parallel | 227 | 0 | 0 |
Fanout / subs=1 | 1 listener | 221 | 0 | 0 |
Fanout / subs=10 | 10 listeners | 1,373 | 0 | 0 |
Fanout / subs=100 | 100 listeners | 4,644 | 0 | 0 |
Fanout / subs=1000 | 1000 listeners | 36,448 | 10 | 0 |
OneTimeListener | On + Emit + remove | 6,595 | 4,142 | 20 |
go test -bench=. -benchmem -benchtime=1s, Go 1.25, Intel Core Ultra 5 125H. Indicative — treat as relative.
The emit path stays zero-allocation up to 1000 subscribers.