go 132 lines · 3 tabs

Server-Sent Events in Go with http.Flusher and Context Cancellation

Shared by codesnips Aug 2026
3 tabs
package sse

type Broker struct {
	subscribers map[chan []byte]struct{}
	subscribe   chan chan []byte
	unsubscribe chan chan []byte
	publish     chan []byte
}

func NewBroker() *Broker {
	b := &Broker{
		subscribers: make(map[chan []byte]struct{}),
		subscribe:   make(chan chan []byte),
		unsubscribe: make(chan chan []byte),
		publish:     make(chan []byte, 16),
	}
	go b.run()
	return b
}

func (b *Broker) run() {
	for {
		select {
		case ch := <-b.subscribe:
			b.subscribers[ch] = struct{}{}
		case ch := <-b.unsubscribe:
			if _, ok := b.subscribers[ch]; ok {
				delete(b.subscribers, ch)
				close(ch)
			}
		case msg := <-b.publish:
			for ch := range b.subscribers {
				select {
				case ch <- msg:
				default: // slow client: drop rather than block fan-out
				}
			}
		}
	}
}

func (b *Broker) Subscribe() chan []byte {
	ch := make(chan []byte, 8)
	b.subscribe <- ch
	return ch
}

func (b *Broker) Unsubscribe(ch chan []byte) {
	b.unsubscribe <- ch
}

func (b *Broker) Publish(msg []byte) {
	b.publish <- msg
}
3 files · go Explain with highlit

This snippet shows how to implement Server-Sent Events (SSE) in Go: a long-lived HTTP response that pushes a stream of text/event-stream messages to a browser without polling. SSE is a good fit for one-directional real-time updates (notifications, live metrics, progress bars) because it rides on plain HTTP, reconnects automatically in the browser, and is far simpler than WebSockets.

The Broker type in broker.go is a tiny in-memory pub/sub hub. It keeps a map of subscriber channels and runs a single event loop in run() that owns that map, so registration, removal, and fan-out all happen on one goroutine and no mutex is needed. Registration and deregistration flow through the subscribe and unsubscribe channels, while Publish pushes a []byte payload to every subscriber. The fan-out uses a select with a default branch: if a subscriber's buffered channel is full, the message is dropped for that client rather than blocking the whole broker. That is deliberate backpressure handling — one slow reader must never stall every other reader.

handler.go contains ServeHTTP, the actual SSE endpoint. It first type-asserts the http.ResponseWriter to an http.Flusher; without flushing, Go's buffering would hold bytes back and the client would see nothing until the connection closed. It sets the mandatory Content-Type: text/event-stream plus Cache-Control: no-cache and Connection: keep-alive headers, then registers a subscriber via broker.Subscribe() and guarantees cleanup with defer broker.Unsubscribe(ch).

The core loop selects over three things: r.Context().Done() fires when the browser disconnects, letting the goroutine exit cleanly instead of leaking; a time.Ticker emits a : keepalive comment line periodically so proxies don't drop an idle connection; and the subscriber channel delivers real events. Each event is written in the wire format data: <payload>\n\n and immediately followed by flusher.Flush() to push it down the socket.

main.go wires it together, starting the broker and firing a background goroutine that publishes JSON ticks. A key pitfall this design avoids is the goroutine leak: because every write path is guarded by context cancellation, closing the tab tears the whole chain down.


Related snips

Share this code

Here's the card — post it anywhere.

Server-Sent Events in Go with http.Flusher and Context Cancellation — share card
Link copied