
Building Real-Time Chat with Go & WebSockets
A practical guide to a production-ready WebSocket chat server in Go β covering Hub design, broadcast patterns, and safe connection management.
Backend Engineering with Go
Part 1 of 2 β’ Series Complete
A 2-part series covering real-world Go backend patterns β from WebSocket servers to rewriting production services.
Table of Contents
Table of Contents (8 sections)
Prerequisites
What You'll Need
Go 1.21+, basic familiarity with goroutines and channels, and a Redis instance on port 6379. Run docker run -d -p 6379:6379 redis:alpine if you don't have one locally.
Project Architecture
The chat system follows a Hub-Client model: a central Hub manages all active connections, while each Client goroutine handles reading from and writing to a single WebSocket connection. Messages flow through channels rather than shared memory β the idiomatic Go approach.
The Hub Struct
1type Hub struct {2 clients map[*Client]bool3 broadcast chan []byte4 register chan *Client5 unregister chan *Client6 mu sync.RWMutex7}89func NewHub() *Hub {10 return &Hub{11 clients: make(map[*Client]bool),12 broadcast: make(chan []byte, 256),13 register: make(chan *Client),14 unregister: make(chan *Client),15 }16}1718func (h *Hub) Run() {19 for {20 select {21 case client := <-h.register:22 h.mu.Lock()23 h.clients[client] = true24 h.mu.Unlock()25 case client := <-h.unregister:26 h.mu.Lock()27 if _, ok := h.clients[client]; ok {28 delete(h.clients, client)29 close(client.send)30 }31 h.mu.Unlock()32 case message := <-h.broadcast:33 h.mu.RLock()34 for client := range h.clients {35 select {36 case client.send <- message:37 default:38 close(client.send)39 delete(h.clients, client)40 }41 }42 h.mu.RUnlock()43 }44 }45}Goroutine Leak Risk
Never close a channel from the receiver side β always close from the sender. In the Hub, the broadcast channel is owned by the Hub, so only Hub.Run() should close it. Clients close their own send channel via the unregister path.
WebSocket Handler
The HTTP handler upgrades the connection using gorilla/websocket, registers the client with the Hub, and spawns two goroutines β one for reading incoming messages and one for draining the send buffer to the wire.
1var upgrader = websocket.Upgrader{2 ReadBufferSize: 1024,3 WriteBufferSize: 1024,4 CheckOrigin: func(r *http.Request) bool {5 return r.Header.Get("Origin") == "https://gochat.manishh.in"6 },7}89func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) {10 conn, err := upgrader.Upgrade(w, r, nil)11 if err != nil {12 log.Printf("upgrade error: %v", err)13 return14 }15 client := &Client{16 hub: hub,17 conn: conn,18 send: make(chan []byte, 256),19 }20 hub.register <- client21 go client.writePump()22 go client.readPump()23}Running the Server
# Clone and run
git clone https://github.com/lordofthemind/gochat
cd gochat
docker compose up -d redis
go run ./cmd/server/main.go
# Server starts at http://localhost:8080
# WebSocket endpoint: ws://localhost:8080/wsSetup Steps
- 1Clone the repository and install dependencies
- 2Start Redis: docker compose up -d redis
- 3Start the server: go run ./cmd/server/main.go
- 4Open http://localhost:8080 in two browser tabs to test messaging
- 5Monitor Redis: redis-cli MONITOR to see pub/sub events in real time
Message Types
All WebSocket messages are JSON-encoded. The server distinguishes between four message types based on the type field in the payload.
| Type | Direction | Payload Fields | Description |
|---|---|---|---|
chat | Client β Server | type, text, room | Regular chat message |
join | Client β Server | type, room | Join a chat room |
presence | Server β Client | type, userId, status | Online/offline notification |
error | Server β Client | type, code, message | Error response from server |
Conclusion
The Hub-Client pattern is battle-tested and scales well to thousands of concurrent connections on a single Go process. For multi-server deployments, swap the in-memory broadcast with Redis pub/sub β the Hub interface stays identical. Check out the go-ws-hub library in the related tools section for a production-ready, zero-dependency implementation.
Related Articles
Continue your learning journey with these handpicked articles.

Related Content
Explore related articles, projects, and tools.