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 !=