Press enter or click to view image in full size
Generated by AI
5 min readJust now
–
Have you ever built a dashboard or live notification system and wondered, “How do I send real-time updates from my Go backend to the browser — without using WebSockets?”
That’s exactly where Server-Sent Events (SSE) shine.
In this article, we’ll explore what SSE is, how it differs from WebSockets, and how to implement it cleanly in Go (Golang) using the standard library (no external dependencies needed).
What are Server-Sent Events?
Server-Sent Events (SSE) allow your server to push data to the client over a single persistent http connection.
Unlike WebSockets, which allow **two-way communication (Server <=> Client) **SSE is **unidirectional: “**server can send data b…
Press enter or click to view image in full size
Generated by AI
5 min readJust now
–
Have you ever built a dashboard or live notification system and wondered, “How do I send real-time updates from my Go backend to the browser — without using WebSockets?”
That’s exactly where Server-Sent Events (SSE) shine.
In this article, we’ll explore what SSE is, how it differs from WebSockets, and how to implement it cleanly in Go (Golang) using the standard library (no external dependencies needed).
What are Server-Sent Events?
Server-Sent Events (SSE) allow your server to push data to the client over a single persistent http connection.
Unlike WebSockets, which allow **two-way communication (Server <=> Client) **SSE is **unidirectional: “**server can send data but the client can’t do the same)
SSE is build on top of the HTTP, which means:
- Works with existing HTTP infrastructure (reverse proxies, load balancers, etc.)
- Automatically reconnects if the connection drops
- Uses simple text-based format (
text/event-stream)
How SSE Works
Here is a simple flow:
- The client opens a connection to specific endpoint: for example
/eventsusingEventSource - The server keeps the connection open and periodically sends the messages
- Each Message starts with prefix
dataand ends with two newlines\n\n
For example
data: {"message": "Hello from Go!"}
Building an SSE server in GO
Let’s build a small SSE server that streams time updates in every 2 seconds
Step 1: Basic Setup
//main.gopackage mainimport ( "fmt" "log" "net/http" "time")func main() { http.HandleFunc("/events", tickHandler) http.Handle("/", http.FileServer(http.Dir("client"))) log.Fatal(http.ListenAndServe(":8080", nil))}func tickHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() for { select { case t := <-ticker.C: fmt.Fprintf(w, "data: %s\n\n", t.Format(time.RFC1123)) if f, ok := w.(http.Flusher); ok { f.Flush() } case <-r.Context().Done(): fmt.Println("Client closed connection") return } }}
Now let’s create a simple client side app
<!--public/index.html--><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Go SSE Example</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; justify-content: center; align-items: center; padding: 20px; } .container { background: white; border-radius: 12px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); padding: 40px; max-width: 600px; width: 100%; } h2 { color: #333; margin-bottom: 30px; text-align: center; font-size: 28px; } #output { max-height: 400px; overflow-y: auto; } #output p { background: #f7fafc; border-left: 4px solid #667eea; padding: 12px 16px; margin-bottom: 10px; border-radius: 4px; color: #2d3748; animation: fadeIn 0.3s ease-in; } #output p:last-child { background: #edf2f7; border-left-color: #764ba2; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } #output::-webkit-scrollbar { width: 8px; } #output::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; } #output::-webkit-scrollbar-thumb { background: #667eea; border-radius: 4px; } </style></head><body><div class="container"> <h2>Server Time Updates</h2> <div id="output"></div></div><script> const output = document.getElementById('output'); const source = new EventSource('http://localhost:8080/events'); source.onmessage = (event) => { const el = document.createElement('p'); el.textContent = event.data; output.appendChild(el); output.scrollTop = output.scrollHeight; }; source.onerror = (err) => { console.error('EventSource failed:', err); };</script></body></html>
Go to localhost:8080 and you should see output something like this. It will get time updates in every 2 seconds.
Press enter or click to view image in full size
Advanced Example — Broadcasting to Multiple Clients
If you want to broadcase to **multiple clients (**like a real dashboard), you can maintain a list of connections. Here is the simplified version of go code. You can leave html as it is:
// main.gopackage mainimport ( "fmt" "net/http" "sync")type Broker struct { clients map[chan string]bool lock sync.Mutex}func newBroker() *Broker { return &Broker{clients: make(map[chan string]bool)}}func (b *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("Access-Control-Allow-Origin", "*") msgChan := make(chan string) b.lock.Lock() b.clients[msgChan] = true b.lock.Unlock() defer func() { b.lock.Lock() delete(b.clients, msgChan) b.lock.Unlock() close(msgChan) }() for msg := range msgChan { fmt.Fprintf(w, "data: %s\n\n", msg) if f, ok := w.(http.Flusher); ok { f.Flush() } }}func (b *Broker) Broadcast(msg string) { b.lock.Lock() defer b.lock.Unlock() for client := range b.clients { client <- msg }}func main() { broker := newBroker() http.HandleFunc("/send", func(w http.ResponseWriter, r *http.Request) { msg := r.URL.Query().Get("msg") broker.Broadcast(msg) fmt.Fprintf(w, "Sent: %s", msg) }) // Serve the events page http.HandleFunc("/events-page", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "client/events.html") }) fmt.Println("📡 Broadcasting SSE server running on :8080") http.ListenAndServe(":8080", nil)}
<!--public/events.html--><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>SSE Events</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; justify-content: center; align-items: center; padding: 20px; } .container { background: white; border-radius: 12px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); padding: 40px; max-width: 600px; width: 100%; } h2 { color: #333; margin-bottom: 30px; text-align: center; font-size: 28px; } #output { max-height: 400px; overflow-y: auto; } #output p { background: #f7fafc; border-left: 4px solid #667eea; padding: 12px 16px; margin-bottom: 10px; border-radius: 4px; color: #2d3748; animation: fadeIn 0.3s ease-in; } #output p:last-child { background: #edf2f7; border-left-color: #764ba2; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } #output::-webkit-scrollbar { width: 8px; } #output::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; } #output::-webkit-scrollbar-thumb { background: #667eea; border-radius: 4px; } </style></head><body><div class="container"> <h2>Live Events Stream</h2> <div id="output"></div></div><script> const output = document.getElementById('output'); const source = new EventSource('/events'); source.onmessage = (event) => { const el = document.createElement('p'); el.textContent = event.data; output.appendChild(el); output.scrollTop = output.scrollHeight; }; source.onerror = (err) => { console.error('EventSource failed:', err); };</script></body></html>
Now you can visit:
http://localhost:8080/events-page
and send a message from another tab:
http://localhost:8080/send?msg=Hello+World
You should see something like this:
Press enter or click to view image in full size
All connected clients will receive the same message instantly.
Key Takeaways
- SSE uses plain HTTP and works out of the box in browsers via
EventSource. - It’s ideal for real-time server-to-client updates.
- Implementing SSE in Go is simple and doesn’t require third-party libraries.
- It’s not a replacement for WebSockets, but a simpler alternative for one-way data streams.
Social networks
Instagram: https://www.instagram.com/khotcholava.dev/