Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
27 Aug 2025 · 6 min read ·Article 58 / 125
Go

58 Implementing Subscriptions with gorilla/websocket

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

58 Implementing Subscriptions with gorilla/websocket

In the modern world of interactive web applications, real-time notifications have become a must-have. From chat and document collaboration to live stock prices — they all require the server to be able to “push” data to the client in real time. One popular pattern for solving this is the Subscription, where the client subscribes to a data stream and the server sends new updates directly to the client.

In this article, I’ll take a deep dive into how to implement a subscription mechanism using the gorilla/websocket library in Go, which has become the de-facto standard for WebSocket communication in the Go ecosystem. We’ll cover the architecture, the code structure, a simulation of the data flow, along with real code examples and production tips.

What Is a Subscription?

Put simply, a subscription is a communication model in which the client registers its interest in a particular event/stream and will receive updates from the server whenever a change occurs. Unlike periodic polling, where the client has to request new information at regular intervals, subscriptions are more efficient because they only push new data when it’s needed.

Case Study

As an example, suppose we have a device monitoring application. Each device periodically sends its status to the backend server, and users want to receive real-time updates whenever a device’s status changes (online/offline, error, and so on).

WebSocket with gorilla/websocket in Go

gorilla/websocket provides a simple API for upgrading a conventional HTTP connection into a full-duplex WebSocket, and it’s a great fit for event-based subscription use cases like this one.

In general, implementing a real-time subscription requires:

  1. The process of upgrading the connection from HTTP→WebSocket.
  2. A subscription registry: a mapping between subscribers (clients) and the events/topics they’re interested in.
  3. A publish mechanism: sending new events/updates to the relevant subscribers.
  4. Connection management — for example, detecting disconnects, retries, and so on.

Let’s break these down one by one.


Architecture Diagram

MERMAID
flowchart LR
    subgraph Server
        U["HTTP/WS Upgrade"] --> R["Registry Subscription"]
        R -->|Device Event| P["Publish Event"]
        P -.->|Push| WSClient1((Client 1))
        P -.->|Push| WSClient2((Client 2))
    end
    Device["Updating Device Status"] --> Server

Explanation:

  • The device pushes its status to the backend.
  • The server receives and processes it, then forwards the update to the clients that have subscribed via WebSocket.

Basic Subscription Implementation

Let’s start with a small bite: how to set up a Go server that accepts WebSocket connections.

Initializing the Handler

go
 1// main.go
 2package main
 3
 4import (
 5    "log"
 6    "net/http"
 7    "github.com/gorilla/websocket"
 8)
 9
10// WebSocket upgrader with a standard configuration
11var upgrader = websocket.Upgrader{
12    ReadBufferSize:  1024,
13    WriteBufferSize: 1024,
14    CheckOrigin: func(r *http.Request) bool { return true },
15}
16
17func main() {
18    http.HandleFunc("/ws", handleWebSocket)
19    log.Println("Listening on :8080")
20    log.Fatal(http.ListenAndServe(":8080", nil))
21}

The WebSocket Handler

go
 1func handleWebSocket(w http.ResponseWriter, r *http.Request) {
 2    conn, err := upgrader.Upgrade(w, r, nil)
 3    if err != nil {
 4        log.Println("Upgrade error:", err)
 5        return
 6    }
 7    defer conn.Close()
 8
 9    // Here we'll register the client, read subscriptions, etc.
10    for {
11        _, msg, err := conn.ReadMessage()
12        if err != nil {
13            log.Println("Read error:", err)
14            break
15        }
16        log.Printf("Received: %s", msg)
17        // Process the subscription message
18    }
19}

The Subscription Registry

In general, we need a data structure that manages who subscribes to which events. For example, a client can subscribe to several devices:

go
 1type Subscription struct {
 2    Conn *websocket.Conn
 3    DeviceIDs map[string]bool // devices that are subscribed to
 4}
 5
 6type Hub struct {
 7    Subscriptions map[*websocket.Conn]*Subscription
 8    Register   chan *Subscription
 9    Unregister chan *Subscription
10    Broadcast  chan DeviceEvent
11}

Explanation:

  • The Hub is the center of all subscriptions, managing registration/removal as well as event broadcasting.
  • A Subscription represents a single client plus its subscription information.

The Subscription Workflow

Let’s look at the subscription workflow step by step.

MERMAID
sequenceDiagram
    participant Client
    participant Server
    participant Device

    Client->>Server: Requests /ws (Upgrade to WS)
    Server->>Client: WebSocket Connection Established
    Client->>Server: { "type": "subscribe", "device_id": "dev123" }
    Device->>Server: Send status update for dev123
    Server->>Client: Push event update (if subscribed)

Full Hub Implementation

Let’s complete the hub so it can register subscriptions and dispatch events.

go
 1type DeviceEvent struct {
 2    DeviceID string      `json:"device_id"`
 3    Status   string      `json:"status"`
 4}
 5
 6func NewHub() *Hub {
 7    return &Hub{
 8        Subscriptions: make(map[*websocket.Conn]*Subscription),
 9        Register:      make(chan *Subscription),
10        Unregister:    make(chan *Subscription),
11        Broadcast:     make(chan DeviceEvent, 128),
12    }
13}
14
15func (h *Hub) Run() {
16    for {
17        select {
18        case s := <-h.Register:
19            h.Subscriptions[s.Conn] = s
20        case s := <-h.Unregister:
21            delete(h.Subscriptions, s.Conn)
22        case event := <-h.Broadcast:
23            for _, s := range h.Subscriptions {
24                if s.DeviceIDs[event.DeviceID] {
25                    s.Conn.WriteJSON(event)
26                }
27            }
28        }
29    }
30}

Updating the Handler

Whenever a client sends a type: "subscribe" message, we update that client’s subscription.

go
 1// Inside handleWebSocket:
 2var subscription = &Subscription{
 3    Conn: conn,
 4    DeviceIDs: make(map[string]bool),
 5}
 6hub.Register <- subscription
 7
 8for {
 9    var req struct {
10        Type string `json:"type"`
11        DeviceID string `json:"device_id"`
12    }
13    err := conn.ReadJSON(&req)
14    if err != nil {
15        // handle unregistration...
16        hub.Unregister <- subscription
17        break
18    }
19    if req.Type == "subscribe" {
20        subscription.DeviceIDs[req.DeviceID] = true
21    }
22}

Simulating Event Pushes from a Device

In a real application, pushing from a device is done over MQTT, HTTP, or gRPC. In this article, we’ll simulate pushing events via a channel.

go
 1func simulateDeviceStatus(hub *Hub) {
 2    deviceIDs := []string{"dev100", "dev200"}
 3    statuses := []string{"online", "offline"}
 4    for {
 5        for _, id := range deviceIDs {
 6            status := statuses[time.Now().UnixNano()%2] // random status
 7            event := DeviceEvent{
 8                DeviceID: id,
 9                Status:   status,
10            }
11            hub.Broadcast <- event
12            time.Sleep(5 * time.Second)
13        }
14    }
15}

Comparison Table

FeatureHTTP PollingWebSocket + Subscription
LatencyHigh (interval)Low (direct push)
Bandwidth usageWastefulEfficient
ScalabilityRather difficultEasier (stateful)
Client implementationEasyRequires a WS lib
ResilienceManual retryMust handle disconnects

Best Practices for Production Scale

  • Heartbeat/Ping: Send pings periodically to detect dead connections.
  • Backpressure: Don’t WriteJSON carelessly; handle slow clients (use buffered channels).
  • Security: Restrict origins, use Auth (JWT/OAuth), and validate subscriptions.
  • Unregister: Make sure every client is unregistered on the server when it disconnects or hits a connection error.
  • Horizontal Scaling: For scaling out, use a shared queue (such as Redis PubSub) between server instances.

Conclusion

With gorilla/websocket, building a real-time subscription service is very feasible and clean. The Hub pattern shown above is a great fit for the event-driven world and can be grown into medium-to-large-scale real-time infrastructure (for very large scale, you can bring in additional technologies such as a message broker/distributed queue).

Bringing push notifications to your Go application is now very achievable — and subscriptions could well become the next strength of your application.

Example implementation repos: https://github.com/gorilla/websocket (official) https://github.com/your-github/real-time-device-subscription (article simulation¹) ¹ Not a real example; feel free to implement the basic version above yourself.


Thanks for reading! If you found this useful, please share it or leave a comment about the challenges you’ve faced implementing real-time subscriptions in your Go projects.

Related Articles

💬 Comments