Using Gorilla WebSocket, Gin Framework, and Hub Architecture
![]()
Repos for this article's reference:
Real-time communication has become an essential feature in modern web applications. Whether it’s messaging, live notifications, or collaborative tools, users expect instant updates without constantly refreshing their browsers.
In this tutorial, you’ll learn how to build a real-time chat application in Go using the Gorilla WebSocket library and the Gin web framework. We’ll implement a Hub-based architecture — separating components like Hub, Client, Room, and Chat, and persisting messages in a database.
By the end, you’ll have a working real-time chat server that supports multiple chat rooms and concurrent clients.
Understanding WebSockets
WebSockets enable full-duplex communication between clients and servers over a single TCP connection. Unlike HTTP polling, WebSockets allow both sides to send data anytime without reopening a connection.
Why WebSockets for Chat Apps?
- Persistent two-way connection.
- Low latency and efficient data transmission.
- Perfect for real-time apps like chat, games, and dashboards.
Why Gorilla WebSocket?
- Simple API for WebSocket upgrades.
- Well-maintained and widely used in Go community.
- Handles concurrent connections efficiently.
Project Setup
Let’s set up the project structure.
mkdir go-chat
cd go-chat
go mod init go-chat
Install dependencies:
go get github.com/gin-gonic/gin
go get github.com/gorilla/websocket
go get gorm.io/gorm
go get gorm.io/driver/sqlite
Create the folder structure:
go-chat/
├── main.go
├── hub/
│ └── hub.go
├── client/
│ └── client.go
├── room/
│ └── room.go
├── chat/
│ └── chat.go
├── models/
│ └── message.go
├── db/
│ └── db.go
└── go.mod
Setting Up the Gin Server
Let’s start with a simple Gin server.
main.go
package main
import (
"github.com/gin-gonic/gin"
"go-chat/db"
"go-chat/hub"
)
func main() {
r := gin.Default()
db.InitDB()
h := hub.NewHub()
go h.Run()
r.GET("/ws/:roomID", func(c *gin.Context) {
roomID := c.Param("roomID")
hub.ServeWs(h, c.Writer, c.Request, roomID)
})
r.Run(":8080")
}
The Hub Architecture
The Hub is the central event dispatcher that:
- Tracks all connected clients.
- Manages message broadcasting.
- Handles registration/unregistration.
hub/hub.go
package hub
type Hub struct {
Clients map[*Client]bool
Broadcast chan *Message
Register chan *Client
Unregister chan *Client
}
func NewHub() *Hub {
return &Hub{
Broadcast: make(chan *Message),
Register: make(chan *Client),
Unregister: make(chan *Client),
Clients: make(map[*Client]bool),
}
}
func (h *Hub) Run() {
for {
select {
case client := <-h.Register:
h.Clients[client] = true
case client := <-h.Unregister:
if _, ok := h.Clients[client]; ok {
delete(h.Clients, client)
close(client.Send)
}
case message := <-h.Broadcast:
for client := range h.Clients {
select {
case client.Send <- message.Data:
default:
close(client.Send)
delete(h.Clients, client)
}
}
}
}
}
Defining the Client
Each connected user is represented as a Client, holding its WebSocket connection and send channel.
client/client.go
package hub
import (
"github.com/gorilla/websocket"
)
type Client struct {
Hub *Hub
Conn *websocket.Conn
Send chan []byte
Room string
}
func (c *Client) ReadPump() {
defer func() {
c.Hub.Unregister <- c
c.Conn.Close()
}()
for {
_, msg, err := c.Conn.ReadMessage()
if err != nil {
break
}
c.Hub.Broadcast <- &Message{Data: msg, Room: c.Room}
}
}
func (c *Client) WritePump() {
defer c.Conn.Close()
for {
msg, ok := <-c.Send
if !ok {
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
c.Conn.WriteMessage(websocket.TextMessage, msg)
}
}
Implementing the Room
Rooms isolate chat channels. Each room can have its own clients and messages.
room/room.go
package hub
type Room struct {
ID string
Clients map[*Client]bool
Hub *Hub
}
func NewRoom(id string, hub *Hub) *Room {
return &Room{
ID: id,
Clients: make(map[*Client]bool),
Hub: hub,
}
}
Database Setup
We’ll use GORM with SQLite for message persistence.
db/db.go
package db
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"go-chat/models"
)
var DB *gorm.DB
func InitDB() {
database, err := gorm.Open(sqlite.Open("chat.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
database.AutoMigrate(&models.Message{})
DB = database
}
models/message.go
package models
import "time"
type Message struct {
ID uint `gorm:"primaryKey"`
RoomID string
Sender string
Content string
CreatedAt time.Time
}
Chat Logic
We’ll define the message structure and persist messages to the DB during broadcast.
chat/chat.go
package hub
import (
"encoding/json"
"go-chat/db"
"go-chat/models"
)
type Message struct {
Data []byte
Room string
}
type ChatMessage struct {
Sender string `json:"sender"`
RoomID string `json:"room_id"`
Content string `json:"content"`
}
func SaveMessage(msg *ChatMessage) {
db.DB.Create(&models.Message{
RoomID: msg.RoomID,
Sender: msg.Sender,
Content: msg.Content,
})
}
func HandleIncomingMessage(h *Hub, raw []byte, room string) {
var msg ChatMessage
json.Unmarshal(raw, &msg)
SaveMessage(&msg)
h.Broadcast <- &Message{Data: raw, Room: room}
}
WebSocket Handler
This function upgrades HTTP requests to WebSocket connections and registers clients to the hub.
hub/ws.go
package hub
import (
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
func ServeWs(h *Hub, w http.ResponseWriter, r *http.Request, room string) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
client := &Client{Hub: h, Conn: conn, Send: make(chan []byte, 256), Room: room}
h.Register <- client
go client.WritePump()
go client.ReadPump()
}
Testing the Chat Application
Run your server:
go run main.go
Then open two browser tabs or use websocat to simulate multiple users:
websocat ws://localhost:8080/ws/room1
Type messages in both tabs — you should see messages broadcasted instantly.
Optional Features
Enhance your app with:
- Private messaging (user-to-user).
- Message history API.
- User authentication with JWT.
- Redis Pub/Sub for distributed chat servers.
- Typing indicators and read receipts.
Best Practices
- Use goroutines efficiently (each client runs in its own read/write loop).
- Handle disconnections cleanly.
- Avoid blocking channels in broadcast logic.
- Use HTTPS and secure WebSocket (wss://) in production.
- Add proper logging and monitoring.
Conclusion
You’ve built a fully functional real-time chat app using:
- Gin for the HTTP API layer,
- Gorilla WebSocket for real-time communication,
- Hub-based architecture for managing rooms and clients, and
- GORM for message persistence.
This structure scales well and can serve as a foundation for production-ready chat systems or any real-time Go application.