Learn Gin - WebSocket, Streaming & SSE
Series/Learn Gin/Episode 16
Episode 16 of 23

Learn Gin - WebSocket, Streaming & SSE

This episode builds realtime communication: WebSocket with gorilla/websocket in Gin handlers, Server-Sent Events with SSEvent, streaming uploads and downloads, and notification and chat use cases.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Up to episode 15, our communication pattern has always been request-response: the client asks, the server answers. This episode 16 opens a new direction: realtime. You'll dissect WebSocket with gorilla/websocket inside Gin handlers, Server-Sent Events (SSE) for one-way notifications, streaming uploads and downloads, and real-world use cases like chat and progress bars.

Why does it matter? Modern applications demand instant updates: notifications appear without refresh, chat messages are sent in milliseconds, and upload progress visibly moves. WebSocket provides a two-way channel, SSE provides a simpler one-way channel, and both run smoothly on top of Gin.

Understanding WebSocket and SSE

The Difference Between the Two Technologies

  • WebSocket: a persistent, two-way TCP connection. Both client and server can send messages at any time. Ideal for chat, games, and realtime collaboration.
  • SSE: a one-way HTTP connection from server to client. The server pushes events; the client only listens. Much simpler, uses plain HTTP, and reconnects automatically.
When to use which
# dua arah, latensi rendah, duplex
websocket: chat, game, kolaborasi
 
# satu arah, sederhana, auto-reconnect
sse: notifikasi, feed harga, progress

For most notifications, SSE is enough and avoids the complexity of managing WebSocket connections on the client side.

WebSocket with gorilla/websocket

Installing and Upgrading the Handler

Start by installing gorilla/websocket:

Install gorilla/websocket
go get github.com/gorilla/websocket
WebSocket echo handler
var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
}
 
func echoHandler(c *gin.Context) {
    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        return
    }
    defer conn.Close()
 
    for {
        _, msg, err := conn.ReadMessage()
        if err != nil {
            break
        }
        if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
            break
        }
    }
}
 
r.GET("/ws", echoHandler)

upgrader.Upgrade(c.Writer, c.Request, nil) turns an HTTP request into a WebSocket connection. This handler echoes back every message it receives. Note the CheckOrigin — in production, return false for unknown domains to prevent cross-site connections.

Simple Chat with a Channel

To spread messages to many clients, use a broadcast pattern via a channel:

Broadcast messages
var broadcast = make(chan []byte, 256)
 
func reader(conn *websocket.Conn) {
    defer conn.Close()
    for {
        _, msg, err := conn.ReadMessage()
        if err != nil {
            break
        }
        broadcast <- msg
    }
}
 
func writer(conn *websocket.Conn) {
    for msg := range broadcast {
        if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
            return
        }
    }
}

broadcast <- msg sends the message to a channel read by all writers. Each connection has its own reader and writer goroutines; writers receive messages from the shared channel. In production, add a client registry and per-room filtering, and always set read/write deadlines to drop idle connections.

Server-Sent Events in Gin

SSEvent and Streaming

Gin provides c.SSEvent to write a single event:

SSE endpoint
func sseHandler(c *gin.Context) {
    c.Header("Content-Type", "text/event-stream")
    c.Header("Cache-Control", "no-cache")
    c.Header("Connection", "keep-alive")
 
    c.Stream(func(w io.Writer) bool {
        c.SSEvent("message", "tick pada " + time.Now().Format("15:04:05"))
        time.Sleep(1 * time.Second)
        return true
    })
}
 
r.GET("/events", sseHandler)

c.SSEvent("message", data) writes an SSE event named message. c.Stream runs the function repeatedly as long as it returns true — the one-way streaming pattern briefly covered in episode 7. The text/event-stream header tells the browser the response will be long-lived.

Streaming Uploads and Downloads

Streaming an Upload from the Body

Large files don't need to be loaded entirely into memory. Stream directly from the request body to a file:

Streaming upload
func streamUploadHandler(c *gin.Context) {
    src := c.Request.Body
    defer src.Close()
 
    dst, err := os.Create("./storage/besar.bin")
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    defer dst.Close()
 
    n, err := io.Copy(dst, src)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    c.JSON(200, gin.H{"bytes": n})
}

io.Copy(dst, src) moves data from the request to disk in small buffers rather than holding the entire body. Combine it with Content-Length to cap the size, and with the rate limit middleware from episode 14 so uploads don't hog bandwidth.

Use Case: Notifications and Chat

Financial Notifications

The most common SSE example is notifications: the server sends an event every time a status changes. The server doesn't wait for the client to ask — the event is pushed directly when the status changes, for example after an order is created or a payment is verified. The implementation is simply an SSE endpoint reading a status channel, exactly the broadcast pattern above.

Chat with WebSocket

For chat, choose WebSocket because it needs two-way, low-latency communication. Every incoming message is broadcast to all connections in the same room. Because this model runs across many goroutines, apply the discipline from episode 12: use channels for sharing data, mutexes for the client registry, and always close goroutines when a connection ends so they don't leak.

Closing

Key takeaways:

  • WebSocket for two-way communication; SSE for one-way pushes.
  • upgrader.Upgrade turns a Gin handler into a WebSocket connection.
  • The reader + writer pattern with channels handles broadcasting to many clients.
  • c.SSEvent and c.Stream build an SSE endpoint in a few lines.
  • io.Copy from c.Request.Body enables streaming uploads.
  • Set CheckOrigin, deadlines, and size limits for connection security.

In the next episode, episode 17, we'll dissect testing & benchmark — unit testing handlers with httptest, gin.CreateTestContext, table-driven tests, mocking services, and benchmarking routes with testing.B to compare allocations and latency.