in-process · generics · Go 1.25

Fire-and-forget
Pub/Sub for Go.

Lightweight, zero-allocation messaging for transient data flows — real-time streaming packets, gaming events, live signals. No persistence, no delivery guarantees, just the fastest one-way path from Publish to Subscribe.

$ go get github.com/F2077/go-pubsub
  • 107ns single-sub publish
  • 0 allocs hot path, any fan-out
  • 10k subscribers fan-out
01

Six properties, one tight package.

Built for the path where a dropped packet is fine and a stalled one is fatal.

Zero persistence

Messages vanish the moment a channel is full or a subscriber times out. No disk, no replay, no acks — the slate is always clean.

Sliding auto-expiry

Per-topic idle timers reset on every successful publish. Stale subscriptions close themselves and surface ErrSubscriptionTimeout.

Carefully locked

A snapshot-then-fan-out design with a mutex per subscriber. Race-clean under -race, deadlock-free by construction.

Bounded capacity

Cap the number of topics per broker; over-cap subscribes return a wrapped ErrSubscriptionCapacityExceeded you can errors.Is.

String topics

One broker, many publishers, many subscribers, many topics. Generic over the payload T — structs, ints, strings, your type.

sync.Pool snapshot

The fan-out recycles its subscriber snapshot through a pool — zero allocations on the hot path, even at ten thousand subscribers.

02

Five lines to a message.

Broker, publisher, subscriber, subscribe, publish. That's the whole contract.

quickstart.go
// 1. Create a broker.
broker, _ := pubsub.NewBroker[string]()

// 2. Bind a publisher and a subscriber to it.
publisher  := pubsub.NewPublisher(broker)
subscriber := pubsub.NewSubscriber(broker)

// 3. Subscribe. WithTimeout arms a sliding timer
//    that resets on every successful publish.
sub, _ := subscriber.Subscribe("alerts",
    pubsub.WithChannelSize(pubsub.Medium),
    pubsub.WithTimeout(200*time.Millisecond),
)

// 4. Publish synchronously. A delivery resets the timer.
publisher.Publish("alerts", "CPU over 90%!")

// 5. Receive.
select {
case msg := <-sub.Ch:
    fmt.Println("Received:", msg)
case err := <-sub.ErrCh:
    log.Println("Timeout:", err)
}

$ go run ./cmd/quickstart  →  Received: CPU over 90%!

03

The hot path allocates nothing.

Median of 3 runs · Intel Core Ultra 5 125H · go test -bench -benchmem

−24% UltraLarge 10k-sub fan-out vs v1.1.1
0 allocations per publish, any scale
107ns single-subscriber publish
Benchmarkns/opB/opallocs/op
PublishSingleSubscriber107.700
MultipleSubscribers (100)6,45900
MultiPub × MultiSub (5×50)15,99630412
UltraLargeSinglePub (10k)2,017,1151,028*0
HighLoadParallel (10k, 180)80,5793,795*0
PublishWithTimeout375.22483

* Non-zero B/op with zero allocs comes from the benchmark harness's drain goroutines, not library code.

Full benchmark table →
04

Know the tool.

Fire-and-forget is a sharp knife. Use it where drops are cheap and stalls are expensive.

Reach for it

  • Real-time streaming-media packet fan-out
  • Low-latency gaming & live-event signals
  • In-process event buses & metrics taps
  • Transient state that's worthless once stale

Reach for something else

  • Persistent queues & durable storage
  • At-least-once / guaranteed delivery
  • Cross-process or network pub/sub
  • Anything where a dropped message is a bug
05

Community & standards.

The project ships the full set of community-health files. Play nice; they're short.