go 59 lines · 8 steps

Server-Sent Events streaming in Go

An HTTP handler that keeps a connection open and pushes events to the browser using the SSE protocol.

Explained by highlit
1package sse
2 
3import (
4 "encoding/json"
5 "fmt"
6 "net/http"
7 "time"
8)
9 
10type Event struct {
11 ID string
12 Data any
13}
14 
15type Broker struct {
16 Subscribe func() (<-chan Event, func())
17}
18 
19func (b *Broker) StreamHandler(w http.ResponseWriter, r *http.Request) {
20 flusher, ok := w.(http.Flusher)
21 if !ok {
22 http.Error(w, "streaming unsupported", http.StatusInternalServerError)
23 return
24 }
25 
26 w.Header().Set("Content-Type", "text/event-stream")
27 w.Header().Set("Cache-Control", "no-cache")
28 w.Header().Set("Connection", "keep-alive")
29 w.Header().Set("X-Accel-Buffering", "no")
30 
31 events, unsubscribe := b.Subscribe()
32 defer unsubscribe()
33 
34 keepalive := time.NewTicker(15 * time.Second)
35 defer keepalive.Stop()
36 
37 for {
38 select {
39 case <-r.Context().Done():
40 return
41 case <-keepalive.C:
42 fmt.Fprint(w, ": ping\n\n")
43 flusher.Flush()
44 case ev, open := <-events:
45 if !open {
46 return
47 }
48 payload, err := json.Marshal(ev.Data)
49 if err != nil {
50 continue
51 }
52 if ev.ID != "" {
53 fmt.Fprintf(w, "id: %s\n", ev.ID)
54 }
55 fmt.Fprintf(w, "data: %s\n\n", payload)
56 flusher.Flush()
57 }
58 }
59}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1SSE works over a long-lived HTTP response by writing text frames and flushing after each one.
  2. 2A select over the request context, a keepalive ticker, and an event channel lets one loop handle disconnect, heartbeat, and delivery.
  3. 3Injecting Subscribe as a function keeps the handler decoupled from how events are actually produced.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Server-Sent Events streaming in Go — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code