Building Chat App with Go Web Socket

9/24/2025

Using Gorilla WebSocket, Gin Framework, and Hub Architecture

Gorilla Icon

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.


References