Architecture
The go-ws package implements a hub-and-spoke WebSocket architecture. A single Hub goroutine owns all mutable state and coordinates client registration, channel subscriptions, and message delivery.
Back to Home
Goroutine Model
┌───────────────────────┐
│ Hub.Run(ctx) │
│ (single goroutine) │
│ │
register ────────►│ clients map │
unregister ──────►│ channels map │
broadcast ───────►│ select loop │
ctx.Done() ──────►│ │
└───────────────────────┘
│ │
┌────┘ └────┐
▼ ▼
┌────────────┐ ┌────────────┐
│ Client A │ │ Client B │
│ readPump() │ │ readPump() │
│ writePump() │ │ writePump() │
└────────────┘ └────────────┘
Each WebSocket connection spawns exactly two goroutines:
| Goroutine | Responsibility |
|---|---|
readPump |
Reads incoming messages from the client, dispatches subscribe/unsubscribe/ping |
writePump |
Drains the client's send channel and writes to the WebSocket; sends periodic pings |
The Hub itself runs in a single goroutine that processes all state mutations via channel-based message passing. This eliminates the need for fine-grained locking in the hot path.
Connection Lifecycle
1. HTTP request arrives at Hub.Handler()
2. gorilla/websocket upgrades the connection
3. Client struct created (send channel, subscriptions map)
4. Client sent to hub.register channel
5. Hub.Run() adds client to clients map
6. readPump() and writePump() goroutines launched
7. Client sends/receives messages...
8. Connection closes (error, timeout, or explicit close)
9. readPump() sends client to hub.unregister channel
10. Hub.Run() removes client from clients map and all channels
11. Empty channels are garbage-collected
Upgrade Configuration
The WebSocket upgrader uses the following defaults:
| Setting | Value |
|---|---|
| Read buffer | 1024 bytes |
| Write buffer | 1024 bytes |
| Origin check | All origins allowed (suitable for local/development use) |
| Read limit | 65536 bytes per message |
| Read deadline | 60 seconds (reset on pong) |
| Write deadline | 10 seconds |
| Ping interval | 30 seconds (server-to-client) |
Write Batching
The writePump batches queued messages into a single WebSocket write frame when multiple messages are buffered. This reduces syscall overhead under high throughput:
// After writing the first message, drain any queued messages
n := len(c.send)
for i := 0; i < n; i++ {
w.Write([]byte{'\n'})
w.Write(<-c.send)
}
Concurrency Model
The Hub uses a layered concurrency approach:
| Layer | Mechanism | Purpose |
|---|---|---|
| Hub event loop | Go channels (register, unregister, broadcast) |
Serialise state mutations |
| Hub public methods | sync.RWMutex on Hub |
Safe reads from outside the event loop (e.g. Stats(), ClientCount()) |
| Channel operations | sync.RWMutex on Hub |
Protect the channels map during Subscribe/Unsubscribe/SendToChannel |
| Client subscriptions | sync.RWMutex on Client |
Protect the per-client subscriptions map |
Back-Pressure
Each client has a buffered send channel (capacity 256). If a client cannot keep up:
- The
broadcastorSendToChannelcall attempts a non-blocking send - If the buffer is full, the client is scheduled for unregistration
- The Hub removes the slow client on the next event loop iteration
This prevents one slow consumer from blocking delivery to other clients.
Graceful Shutdown
When the context passed to Hub.Run() is cancelled:
- The select loop receives
ctx.Done() - All client
sendchannels are closed - Each
writePumpdetects the closed channel and sends a WebSocket close frame - All client entries are removed from the Hub
Hub.Run()returns
ctx, cancel := context.WithCancel(context.Background())
go hub.Run(ctx)
// Later, to shut down cleanly:
cancel()
See Also
- Message-Types -- Wire format for all message types
- Channel-Subscriptions -- How channel routing works
- Process-Streaming -- Process-specific streaming helpers