SEDA bus · Go · 0.1.0

No pool needed — goroutines already fit the shape.

seda-bus-go needs neither a hand-rolled pool (Rust, C++) nor a borrowed built-in one (Java, C#) — Go's own primitives are close enough to the SEDA shape that no pool abstraction was needed at all. It's also the only one of the seven ports with an actually clean go test -race run behind it.

Worker pool
None — a bare go bus.drain(ch) goroutine per scheduled drain
True stage parallelism
Yes — the Go runtime's M:N scheduler
Dependencies
ra-common-go only
Envelope
messaging.Envelope, aliased as sedabus.Envelope via a real Go type alias
Routing slip
DynamicRoutingSlip — LIFO via append/pop at a slice's end
Concurrency limits
Buffered channels used as counting semaphores
Guaranteed delivery
No — in-memory only
Batch size
16 envelopes per drain
Publish timeout
*time.Duration, deliberately not context.Context
Race/sanitizer verified
Yes — go test -race, clean, five repeated runs

How it implements the design

The language's own primitives, used as idiomatically as possible.

Goroutines are cheap enough (a few KB of stack, M:N-scheduled onto OS threads by the Go runtime) that pooling them would fight the language rather than match it — this port's biggest structural departure from the shared design's “one shared worker pool.” A bus-wide buffered channel used as a counting semaphore still bounds how many drain goroutines run concurrently, playing the role a sized pool plays elsewhere, layered on top of each stage's own per-channel semaphore — Go's standard idiom for a semaphore, since the language ships no counting-semaphore type in its standard library. Go has no default/named parameters, so the envelope's optional slip/sender/headers become the functional-options pattern (MakeEnvelope(to, payload, WithSlip(...), WithSender(...))) instead.

sync.Mutex + sync.Cond back the per-stage queue, itself a container/list.List rather than a slice — repeatedly re-slicing a FIFO from the front never shrinks the backing array, a real memory-growth gotcha for a long-running bus, while a linked list gives true O(1) push/pop at both ends. sync.Cond has no built-in timeout, so a time.AfterFunc timer broadcasts on expiry and the waiter re-checks its own deadline on wake — the same pattern other ports implement with wait_until/Monitor.Wait(timeout). And since nothing here is a pool Shutdown can join, an atomic in-flight counter (the same technique C# needs for the same reason) lets awaitDrain wait for zero in-flight and zero depth, not depth alone.

The one real bug found and fixed: a lost-wakeup race in schedule(). It silently gave up when a bus-wide permit acquisition failed, with no guarantee anything would ever retry the channel — if a channel's own producer had already finished and its last drain goroutine lost that race, its remaining backlog sat queued until Shutdown's timeout expired. This showed up intermittently in this very benchmark's chan config across two separate sessions. The fix: releaseBusPermit now sweeps every registered channel with a non-empty queue and re-offers it to schedule() whenever a permit frees, not just the channel that happened to release it. Verified: go test -race clean, 10 consecutive full benchmark runs (30 chan trials) with zero drain failures, versus 2-of-3 failing before the fix under the same conditions.

Results

Strong channel-splitting gains, and a clean bill of health on races.

Capacity curve — sustained throughput under real, bounded back-pressure
ConfigSustained eps0.5×1.0×1.5×
cap1179,7060.950.940.65
cap8399,5580.950.890.60
Firehose throughput (envelopes/sec, unthrottled — diagnostic, not capacity planning)
Configepsvs. seq
seq (1 producer)409,220
par (8 producers, 1 channel)368,8380.90×
chan (8 producers, 8 channels)1,517,2943.71×

Reading the numbers

par's flat ratio is misleading on its own — read it against chan instead.

par barely beating (here, slightly trailing) seq looks like Go gets little from parallelism, but that's misleading rather than wrong: this port's seq is itself already backlogged (p50/elapsed 0.46, a real sustained-backlog case), not a clean baseline — comparing par against an already-degraded seq produces a ratio that says nothing about whether parallelism helped. chan's 3.71× gain against the same seq is the number that actually answers the question, and it's one of the strongest channel-splitting gains in the whole comparison.

The full cross-language results, including the schedule() race investigation

Source

Read the code, or the rest of the family.

seda-bus-go on GitHub — source, its own DESIGN.md, and the test suite.