Channel Subscriptions
Channels provide targeted message delivery. Rather than broadcasting every message to every client, channels allow clients to opt in to specific message streams. This is the primary mechanism for efficient, scoped communication.
Back to Home
Overview
A channel is simply a named string (e.g. "process:build-1", "events:deployments", "chat:room-42"). The Hub maintains a map of channel names to sets of subscribed clients. When a message is sent to a channel, only subscribed clients receive it.
Hub
┌───────────────────────────┐
│ │
│ channels map: │
│ │
│ "process:build-1" │
│ ├── Client A │
│ └── Client B │
│ │
│ "process:test-3" │
│ └── Client A │
│ │
│ "events:deploy" │
│ └── Client C │
│ │
└───────────────────────────┘
In this example, a message sent to "process:build-1" reaches Client A and Client B, but not Client C.
Subscribe Flow
Client-Side (JavaScript)
const socket = new WebSocket("ws://localhost:8080/ws");
// Subscribe to a channel
socket.send(JSON.stringify({
type: "subscribe",
data: "process:build-1"
}));
What Happens Internally
- The client's
readPumpgoroutine receives the message - It parses the JSON and identifies
type: "subscribe" - It extracts the channel name from the
datafield - It calls
hub.Subscribe(client, channel) - The Hub creates the channel if it does not already exist
- The client is added to the channel's subscriber set
- The channel name is recorded in the client's
subscriptionsmap
// Inside readPump
case TypeSubscribe:
if channel, ok := msg.Data.(string); ok {
c.hub.Subscribe(c, channel)
}
No confirmation message is sent back to the client. The subscription takes effect immediately.
Unsubscribe Flow
Client-Side (JavaScript)
socket.send(JSON.stringify({
type: "unsubscribe",
data: "process:build-1"
}));
What Happens Internally
- The
readPumpreceives the message - It calls
hub.Unsubscribe(client, channel) - The client is removed from the channel's subscriber set
- If the channel has no remaining subscribers, it is deleted from the channels map (garbage collection)
- The channel name is removed from the client's
subscriptionsmap
Automatic Cleanup on Disconnect
When a client disconnects (connection error, timeout, or explicit close), the Hub automatically removes the client from all subscribed channels:
1. readPump detects connection error
2. Sends client to hub.unregister channel
3. Hub.Run() processes unregister:
a. Removes client from clients map
b. Closes client's send channel
c. Iterates client.subscriptions
d. Removes client from each channel
e. Deletes any channels that become empty
This means clients never need to explicitly unsubscribe before disconnecting.
Channel Naming Conventions
Channel names are arbitrary strings. The package uses process:{id} internally for process streaming (see Process-Streaming), but you can use any naming scheme:
| Pattern | Example | Use Case |
|---|---|---|
process:{id} |
process:build-1 |
Process output streaming |
events:{category} |
events:deployments |
Categorised event streams |
chat:{room} |
chat:room-42 |
Chat rooms |
user:{id} |
user:alice |
Per-user notifications |
{custom} |
system-alerts |
Any application-specific channel |
Sending to a Channel
Server-Side API
// Send a custom message to channel subscribers
err := hub.SendToChannel("events:deployments", ws.Message{
Type: ws.TypeEvent,
Data: map[string]any{
"event": "deploy_started",
"data": map[string]string{"version": "1.2.3"},
},
})
The SendToChannel method:
- Sets the
TimestampandChannelfields on the message - Marshals the message to JSON
- Acquires a read lock on the Hub
- Looks up the channel in the channels map
- Sends the serialised message to each subscriber's
sendchannel (non-blocking) - Returns
nilif the channel has no subscribers (this is not an error)
No Subscribers Behaviour
Sending to a channel with no subscribers silently succeeds. This is by design -- the sender does not need to know whether anyone is listening.
// This returns nil even if nobody is subscribed
err := hub.SendToChannel("empty-channel", msg)
// err == nil
Querying Subscriptions
Hub-Level
// How many channels exist?
count := hub.ChannelCount()
// How many clients are subscribed to a channel?
subs := hub.ChannelSubscriberCount("process:build-1")
Client-Level
// Get a copy of a client's subscriptions
channels := client.Subscriptions()
// Returns: ["process:build-1", "events:deployments"]
Multiple Subscriptions
A client can subscribe to any number of channels simultaneously. Each subscription is independent:
// Subscribe to multiple channels
socket.send(JSON.stringify({ type: "subscribe", data: "process:build-1" }));
socket.send(JSON.stringify({ type: "subscribe", data: "process:test-3" }));
socket.send(JSON.stringify({ type: "subscribe", data: "events:deploy" }));
Subscribing to the same channel twice is safe and idempotent -- the client is simply already present in the set.
See Also
- Architecture -- How the Hub manages channels internally
- Message-Types -- Subscribe/unsubscribe message format
- Process-Streaming -- The
process:{id}channel convention