WebSocket in Go: Building Advanced-Level Real-Time Applications

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Beyond Simple WebSocket in Go



Basic WebSocket examples in Go usually show a simple echo connection. In real production projects — from trading terminals to multiplayer games and notification systems — much more is required: room management, Graceful Shutdown, scaling, and proper error handling. In this article, we will analyze an advanced pattern for building real-time applications in Go using the gorilla/websocket library.



We will create an architecture based on Hub (central dispatcher) and Client (individual connection), add room support, a fault-tolerance mechanism, and show how to properly shut down the server.



Hub and Client Architecture: The Foundation of a Real-Time Server



Why Hub?

Each WebSocket client is a separate goroutine. Managing them directly from an HTTP handler is dangerous: it leads to data races and memory leaks. The Hub pattern solves this problem by acting as a single channel for registration, removal, and message broadcasting.



Hub Implementation

package main

import ( "log" "sync")

type Client struct { hub *Hub send chan []byte room string}

type Hub struct { clients map[*Client]bool broadcast chan Message register chan *Client unregister chan *Client mu sync.RWMutex}

type Message struct { Room string Data []byte Sender *Client}

func NewHub() *Hub { return &Hub{ clients: make(map[*Client]bool), broadcast: make(chan Message, 256), register: make(chan *Client), unregister: make(chan *Client), }}

func (h *Hub) Run() { for { select { case client := <-h.register: h.mu.Lock() h.clients[client] = true h.mu.Unlock() log.Printf("Client connected. Total: %d", len(h.clients))

case client := <-h.unregister: h.mu.Lock() if _, ok := h.clients[client]; ok { delete(h.clients, client) close(client.send) } h.mu.Unlock()

case message := <-h.broadcast: h.mu.RLock() for client := range h.clients { // Send only to the target room if client.room == message.Room || message.Room == "" { select { case client.send <- message.Data: default: // If the channel is full — disconnect the client h.mu.RUnlock() h.unregister <- client h.mu.RLock() } } } h.mu.RUnlock() } }}


Key points:

  • Channels — register, unregister, broadcast — safely synchronize goroutines.
  • RWMutex — allows simultaneous reading of the client list but blocks writes.
  • Room-based routing — a message is sent only to clients in the same room.


Room and Subscription Management



Rooms are logical groups of clients. For example, in a chat, each conversation has its own room. In our Hub, the room is set by the room field in Client. Let's look at how a client subscribes to a room.



HTTP Handler with WebSocket Upgrade

import (    "net/http"    "github.com/gorilla/websocket")

var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true // For production, replace with origin check },}

func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err !=

Blogs

Book Recommendations