WebSockets Explained (Golang): A Practical Guide to Real-Time Apps
Why Another WebSocket Tutorial?
When I was learning WebSockets, I struggled to find a resource that balanced simplicity with depth. Most tutorials were either too basic or too complex. This guide is for those who, like me, want to build a real chat app while understanding the reasoning behind each decision
What Are We Building?
A real-time chat application with:
Multiple chat rooms
Real-time messaging
Online user tracking
Join/leave notifications
Project Structure
chat-app/
├── websockets/
│ ├── hub.go # Central message & connection manager
│ ├── client.go # Individual connection handler
│ └── websocket.go # HTTP → WebSocket setup
└── main.go # Application entry point
The Core Concepts
WebSockets
Traditional HTTP is like sending letters that you have to keep asking, "got any mail for me?" WebSockets are like having a phone call, both sides can talk whenever they want.
The Hub Pattern: Air Traffic Control for Messages
Think of the Hub as an air traffic controller. It:
tracks all connected users
Makes sure messages get to the right rooms
Handles users joining and leaving
Client Architecture
Each connection runs two concurrent goroutines:
readPump: Receives messages
writePump: Sends messages and maintains connection - ping-pong
Prerequisites
You should know some Go and understand what goroutines and channels are
Client Architecture
Let's Break Down Each File
1. hub.go: The Brain of Our Operation
The Hub manages all active WebSocket connections and handles message routing. It's the central component that:
Tracks active clients in each chat room
Handles new client connections
Manages client disconnections
Sends messages between clients in the same room
Maintains the list of online users per room
Key Features:
Room-based chat (multiple chat rooms)
User join/leave notifications
Online user list per room
Message broadcasting to room members
In a chat app, handling tasks sequentially is too slow:
Multiple connections need to be managed.
Messages must be processed concurrently.
Slow clients shouldn't block the system
Goroutines: Run tasks concurrently
Channels: Allow safe communication between goroutines
CODE WALKTHROUGH:
package websockets
import (
"encoding/json"
"log"
"strings"
)
// Message structure
type Message struct {
Type string `json:"type"` // Message types: chat, user_joined, user_left, online_users
Content string `json:"content"` // The content
RoomName string `json:"room"` // The room code or id
Username string `json:"username"` // The sender's username
}
// Hub struct maintains as we said its the Air traffic controller
type Hub struct {
clients map[*Client]bool // All connected clients
rooms map[string]map[*Client]bool // rooms:{room1:{client1, client2}}
broadcast chan Message // Channel for broadcasting messages
register chan *Client // Channel for client registration
unregister chan *Client // Channel for client disconnection
}
//constructor
func NewHub() *Hub {
return &Hub{
clients: make(map[*Client]bool),
rooms: make(map[string]map[*Client]bool),
broadcast: make(chan Message),
register: make(chan *Client),
unregister: make(chan *Client),
}
}
func (h *Hub) Run() {
for {
select {
case client := <-h.register:
h.handleRegister(client)
case client := <-h.unregister:
h.handleUnregister(client)
case message := <-h.broadcast:
h.handleBroadcast(message)
}
}
}
//----------------------------ROOM MANAGEMENT-------------------------------------
func (h *Hub) handleRegister(client *Client) {
// Create room if doesnt exists
if _, exists := h.rooms[client.room]; !exists {
h.rooms[client.room] = make(map[*Client]bool)
}
// Add client to room in both room and client list check HUB struct
h.rooms[client.room][client] = true
h.clients[client] = true
// Send online users list
h.broadcastRoomUsers(client.room)
}
func (h *Hub) handleUnregister(client *Client) {
if _, exists := h.clients[client]; !exists {
return
}
// Remove client
delete(h.clients, client)
delete(h.rooms[client.room], client)
// Notify room and update user list
h.handleBroadcast(Message{
Type: "user_left",
Content: client.username + " left the room",
RoomName: client.room,
Username: client.username,
})
h.broadcastRoomUsers(client.room)
// Delete if room empty
if len(h.rooms[client.room]) == 0 {
delete(h.rooms, client.room)
}
}
//----------------------------BROADCASTING MESSAGES-------------------------------------
func (h *Hub) broadcastRoomUsers(room string) {
users := []string{}
if roomClients, exists := h.rooms[room]; exists {
for client := range roomClients {
users = append(users, client.username)
}
}
h.handleBroadcast(Message{
Type: "online_users",
Content: strings.Join(users, ","),
RoomName: room,
})
}
func (h *Hub) handleBroadcast(msg Message) {
jsonMsg, err := json.Marshal(msg)
if err != nil {
log.Printf("Error marshaling message: %v", err)
return
}
// Send to all clients in the room
if roomClients, exists := h.rooms[msg.RoomName]; exists {
for client := range roomClients {
select {
case client.send <- jsonMsg:
// Message sent successfully
default:
// Client's buffer is full, remove them
close(client.send)
delete(h.clients, client)
delete(h.rooms[msg.RoomName], client)
}
}
}
}
2. client.go: Individual Connection Handler
Each client manages its own WebSocket connection with two concurrent processes. Manages their Own WebSocket connection
Key Features:
Concurrent message handling (read/write)
Connection health monitoring (ping-pong)
Automatic clean-up on disconnect
Buffer management for messages
CONCEPT TIME→
*Connection Health (Ping/Pong): Think of Ping/Pong like a heartbeat check for your WebSocket connection
EXAMPLE SCENARIO:
Client connects to server
Every
pingPeriod
(54 seconds), server sends: "ping"Client must respond with: “pong” within
pongWait
(60 seconds)If no pong received: "Connection dead, clean-up time!"
*Time Management: Tickers are like alarm clocks that ring at regular intervals.
REAL WORLD ANALOGY FOR ABOVE TWO:
a teacher checking if students are awake
Ticker rings every 1 minute
You say "ping!" and wait for "pong!"
No response? Student's asleep (connection dead)
*Write Deadlines: Like setting a timeout for message delivery.
EXAMPLE MIGHT BE:
You say "Hi!" to some girl wearing headphones
You wait exactly 10 seconds for a response
They don't hear you (timeout reached)
You walk away pretending it never happened 😅
package websockets
import (
"log"
"time"
"github.com/gorilla/websocket"
)
// Connection management constants
// These are crucial production applications
const (
// Time allowed to write a message to the users
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer
pongWait = 60 * time.Second
// Send pings to peer with this period
// Must be less than pongWait
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer
maxMessageSize = 512
)
// Client represents a connected websocket user
type Client struct {
hub *Hub // Reference to our central hub broadcasting messages
conn *websocket.Conn // our WebSocket connection
send chan []byte // channel for outbound messages
room string // Current room name
username string // User's display name
}
//----------------------------READING MESSAGES-------------------------------------
func (c *Client) readPump() {
// deferred Cleanup on function exit
// This ensures resources are freed when connection ends
defer func() {
// Notify air traffic controller(HUB) that client is disconnecting
c.hub.unregister <- c
// Close the physical connection
c.conn.Close()
}()
// Configure connection constraints
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error {
// Reset deadline when pong is received
c.conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
// Main read loop
for {
// method to read a message
_, message, err := c.conn.ReadMessage()
if err != nil {
// Check errors
// bunch of errors check docs for more information
if websocket.IsUnexpectedCloseError(err,
websocket.CloseGoingAway,
websocket.CloseAbnormalClosure) {
log.Printf("error: %v", err)
}
break // Exit loop on any error
}
// Create message
msg := Message{
Type: "chat",
Content: string(message), // Convert bytes to string
RoomName: c.room,
Username: c.username,
}
// Forward message to hub for broadcasting
c.hub.broadcast <- msg
}
}
//----------------------------WRITING MESSAGES-------------------------------------
func (c *Client) writePump() {
// Create ticker for pinging this maintains connection health
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
// Set write deadline for each message
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
// Channel closed by hub
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
// Get the next writer for the connection
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
// Write the message
w.Write(message)
// Close the writer
w.Close()
case <-ticker.C:
// Send ping
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
3. websocket.go: Connection Setup
This file handles the HTTP to WebSocket upgrade process.
Connection Flow:
Client connects to /ws/:room?username=xxx
Check room name and username
Upgrade WebSocket connection
Create new client
Start message handling
package websockets
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
// upgrader converts HTTP connections to WebSocket connections
var upgrader = websocket.Upgrader{
// buffersize affect memory usage and performance
ReadBufferSize: 1024, // Adjust based on expected message size
WriteBufferSize: 1024,
// accepting all for dev purpose
CheckOrigin: func(r *http.Request) bool {
return true // Development only In production application implement CORS correctly
},
}
// Step 1: Extract and check connection parameters
// Step 2: Upgrade HTTP connection to WebSocket
// Step 3: Create new client
// Step 4: Register client with hub
// Step 5: Start client read/write pumps
// This is where new WebSocket connections are established
func HandleWebSocket(h *Hub) gin.HandlerFunc {
return func(c *gin.Context) {
// Step 1: Extract and check connection parameters
room := c.Param("room")
username := c.Query("username")
// Validate required fields
if room == "" || username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "room and username are required"})
return
}
// Step 2: Upgrade HTTP connection to WebSocket
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("Failed to upgrade connection: %v", err)
return
}
// Step 3: Create new client
client := &Client{
hub: h,
conn: conn,
send: make(chan []byte, 256), // Buffer size affects memory usage
room: room,
username: username,
}
// Step 4: Register client with hub
h.register <- client
// Create and broadcast join message
joinMessage := Message{
Type: "user_joined",
Content: username + " joined the room",
RoomName: room,
Username: username,
}
h.broadcast <- joinMessage
// Step 5: Start client read/write pumps
go client.writePump() // Handles sending messages to client
go client.readPump() // Handles receiving messages from client
}
}
4. main.go: Where all Connects
package main
import (
"chat-app/websockets"
"log"
"github.com/gin-gonic/gin"
)
// Step 1: Initialize router and hub
// Step 2: Setup routes
// Step 3: start server
func main() {
// Initialize router and hub
r := gin.Default()
hub := websockets.NewHub()
go hub.Run()
// Set up routes
r.GET("/ws/:room", websockets.HandleWebSocket(hub))
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// Start server
log.Println("Server starting on :8080")
if err := r.Run(":8080"); err != nil {
log.Fatal("Server failed to start:", err)
}
}
Testing Your Creation
1. Connect to a room: `ws://localhost:8080/ws/room1?username=Eren`
2. Connect to a room: `ws://localhost:8080/ws/room1?username=Mikasa`
3. Send messages: Just send text through the WebSocket connection
4. Receive updates: Listen for WebSocket messages for chat/status updates
Challenges?
Implement typing indicators
Add authentication
Store message history
Final Thoughts
We've built a complete real-time chat application using WebSockets and Go. Through this implementation, we've covered essential concepts like connection management, message broadcasting, and room-based communication.
To build upon this foundation, consider doing the challenges mentioned above. The structure we have makes it straightforward to extend the functionality with maintainable code.