Creating a Chat Application with Go, Gorilla WebSocket, and jQuery

Creating a Chat Application with Go, Gorilla WebSocket, and jQuery

Featured on Hashnode

In this blog, we will explore how to build a real-time chat application using Go (Golang), Gorilla WebSocket, and jQuery. Real-time chat applications require quick and seamless communication between users, and WebSockets make this possible by enabling continuous, two-way communication.

Why WebSockets?

WebSockets are an advanced technology that allows a persistent, bidirectional connection between the client (e.g., browser) and the server. Unlike traditional HTTP, where a connection is opened for each request and response, WebSockets establish a single connection that remains open throughout the session. This reduces overhead and ensures low-latency communication.

WebSocket Workflow

The WebSocket protocol works as shown in the diagram:

  1. HTTP Connection (Upgrade Request):
    The process begins with a standard HTTP connection where the client sends an "Upgrade" request to the server, asking to switch to the WebSocket protocol.

  2. 101 Switching Protocols:
    If the server supports WebSockets, it responds with an HTTP 101 Switching Protocols status code. This confirms that the connection is upgraded.

  3. WebSocket Communication:
    Once the connection is upgraded, bidirectional communication starts. Both the client and server can send messages to each other over the same connection without repeatedly re-establishing connections.

  4. Connection Close:
    When the communication ends, either the client or the server can close the WebSocket connection.

This workflow ensures efficient and fast communication, making WebSockets ideal for applications like chats, live notifications, or real-time updates.

Setting Up the Project

To build a real-time chat application using Go, Gorilla WebSocket, and jQuery, it’s important to set up the project directory structure correctly. Below is the step-by-step guide and methods to include all dependencies and necessary files.

Project Directory Structure

1. Initializing the Project

Start by creating the Go module and installing required dependencies.

  1. Initialize the Go Module:
    Run the following command to initialize a Go module in the project root:
go mod init go-websocket-blog
  1. Install Dependencies:
    Install the necessary libraries:
go get github.com/gorilla/websocket
go get github.com/bmizerany/pat
go get github.com/CloudyKit/jet/v6

2. Adding Reconnecting WebSocket

To include the reconnecting-websocket.min.js library for automatic reconnections, you can fetch it either automatically (Linux/Mac) or manually (Windows).

Automatic Download (Linux/Mac)

Run the following script to download the file into the correct directory:

#!/bin/bash

# Create required directories
mkdir -p assets/javascript

# Download reconnecting-websocket.min.js
curl -o assets/javascript/reconnecting-websocket.min.js \
https://raw.githubusercontent.com/joewalnes/reconnecting-websocket/master/reconnecting-websocket.min.js

echo "reconnecting-websocket.min.js has been downloaded successfully!"

Manual Download (Windows)

  1. Go to the following URL:
    https://github.com/joewalnes/reconnecting-websocket/blob/master/reconnecting-websocket.min.js

  2. Copy the entire code from the file.

  3. Create a file named reconnecting-websocket.min.js inside the assets/javascript/ directory.

  4. Paste the copied code into the file and save it.

3. Full Bash Script to Generate the Project Structure

To automate the entire setup, use the following script:

#!/bin/bash

# Create project structure
mkdir -p go-websocket-blog/assets/css
mkdir -p go-websocket-blog/assets/javascript
mkdir -p go-websocket-blog/cmd
mkdir -p go-websocket-blog/pkg/handlers
mkdir -p go-websocket-blog/pkg/routes
mkdir -p go-websocket-blog/templates

cd go-websocket-blog

# Create files
touch assets/css/styles.css
touch assets/javascript/main.js
touch cmd/main.go
touch pkg/handlers/handlers.go
touch pkg/routes/routes.go
touch templates/home.html

# Initialize Go module
go mod init go-websocket-blog

# Add dependencies
go get github.com/gorilla/websocket
go get github.com/bmizerany/pat
go get github.com/CloudyKit/jet/v6

# Fetch reconnecting-websocket.min.js
curl -o assets/javascript/reconnecting-websocket.min.js \
https://raw.githubusercontent.com/joewalnes/reconnecting-websocket/master/reconnecting-websocket.min.js

# Print success message
echo "Project structure created successfully with dependencies and reconnecting-websocket!"

Core WebSocket Handlers

The handlers.go file serves as the heart of this application, managing WebSocket connections, communication, and rendering templates. We'll break down the code function by function, explaining its purpose and behavior. Let’s get started!

Setting Up WebSocket Connections and Templates

var (
    wsChannel       = make(chan WsPayload)
    clients         = make(map[*websocket.Conn]string)
    views           = jet.NewSet(
        jet.NewOSFileSystemLoader("./templates"),
        jet.InDevelopmentMode(),
    )
    upgradeConnection = websocket.Upgrader{
        ReadBufferSize:  1024,
        WriteBufferSize: 1024,
        CheckOrigin: func(r *http.Request) bool { return true },
    }
)

Explanation:

  • wsChannel: A Go channel for passing messages between WebSocket handlers.

  • clients: A map to track active WebSocket connections and their associated usernames.

  • views: Configures Jet template engine to load templates from the ./templates directory.

  • upgradeConnection: Configures WebSocket settings, allowing all origins (CheckOrigin returns true).

This setup enables managing WebSocket connections and rendering HTML templates dynamically.

Home Page Handler

func Home(w http.ResponseWriter, r *http.Request) {
    log.Println("Rendering home page")
    err := renderPage(w, "home.html", nil)
    if err != nil {
        log.Println("Error rendering home page:", err)
    }
}

Explanation:

  • The Home function renders the home.html template using the renderPage helper function.

  • If rendering fails, it logs the error. This provides a simple way to serve the application's main interface.

WebSocket Endpoint

func WsEndpoint(w http.ResponseWriter, r *http.Request) {
    log.Println("Client attempting to connect to WebSocket endpoint")

    ws, err := upgradeConnection.Upgrade(w, r, nil)
    if err != nil {
        log.Println("WebSocket upgrade failed:", err)
        return
    }

    log.Println("Client successfully connected to WebSocket endpoint")

    response := WsJsonResponse{
        Message: "<em><small>Connected to server</small></em>",
    }

    clients[ws] = ""
    err = ws.WriteJSON(response)
    if err != nil {
        log.Println("Error writing JSON response:", err)
        return
    }

    go ListenForWs(ws)
}

Explanation:

  • Upgrade Connection: Converts an HTTP connection into a WebSocket connection using websocket.Upgrader.

  • Initial Response: Sends a confirmation message to the client.

  • Client Management: Adds the WebSocket connection to the clients map.

  • Message Listening: Launches the ListenForWs function in a goroutine to handle client messages asynchronously.

Listening to Client Messages

func ListenForWs(conn *websocket.Conn) {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("Recovered from error: %v", r)
        }
    }()

    var payload WsPayload
    for {
        if err := conn.ReadJSON(&payload); err != nil {
            log.Println("Error reading WebSocket message:", err)
            return
        }

        payload.Conn = conn
        wsChannel <- payload
    }
}

Explanation:

  • Reads JSON messages from the client and stores them in the payload.

  • Adds the client’s connection to the payload and sends it to wsChannel for processing.

  • Uses defer to gracefully handle panics and clean up.

Handling Messages on the WebSocket Channel

func ListenToWsChannel() {
    for {
        e := <-wsChannel
        var response WsJsonResponse

        switch e.Action {
        case "username":
            clients[e.Conn] = e.Username
            response.Action = "list_users"
            response.ConnectedUsers = getUserList()
            broadcastToAll(response)

        case "left":
            delete(clients, e.Conn)
            response.Action = "list_users"
            response.ConnectedUsers = getUserList()
            broadcastToAll(response)

        case "broadcast":
            response.Action = "broadcast"
            response.Message = fmt.Sprintf("<strong>%s</strong>: %s", e.Username, e.Message)
            broadcastToAll(response)
        }
    }
}

Explanation:

  • Listening to wsChannel: Continuously waits for messages sent through the channel.

  • Message Handling:

    • username: Updates the client's username and broadcasts the user list.

    • left: Removes a client and updates the user list.

    • broadcast: Sends the client’s message to all connected users.

Helper Functions

Retrieve User List

func getUserList() []string {
    var userList []string
    for _, username := range clients {
        if username != "" {
            userList = append(userList, username)
        }
    }
    sort.Strings(userList)
    return userList
}

Purpose: Collects all usernames from clients, sorts them alphabetically, and returns the list.

Broadcast Messages

func broadcastToAll(response WsJsonResponse) {
    for client := range clients {
        if err := client.WriteJSON(response); err != nil {
            log.Printf("Error sending message to client: %v", err)
            _ = client.Close()
            delete(clients, client)
        }
    }
}

Purpose: Sends a JSON response to all connected WebSocket clients. Removes clients that fail to receive the message.

Render Templates

func renderPage(w http.ResponseWriter, tmpl string, data jet.VarMap) error {
    view, err := views.GetTemplate(tmpl)
    if err != nil {
        log.Println("Error loading template:", err)
        return err
    }

    if err := view.Execute(w, data, nil); err != nil {
        log.Println("Error executing template:", err)
        return err
    }
    return nil
}

Purpose: Fetches and renders an HTML template with dynamic data using Jet.

Here’s the complete handlers.go file for you to copy and use

package handlers

import (
    "fmt"
    "log"
    "net/http"
    "sort"

    "github.com/CloudyKit/jet/v6"
    "github.com/gorilla/websocket"
)

// Global variables for managing WebSocket connections
var (
    wsChannel       = make(chan WsPayload)                // Channel for handling WebSocket messages
    clients         = make(map[*websocket.Conn]string)   // Map of connected clients and their usernames
    views           = jet.NewSet(                        // Template engine configuration
        jet.NewOSFileSystemLoader("./templates"),
        jet.InDevelopmentMode(),
    )
    upgradeConnection = websocket.Upgrader{
        ReadBufferSize:  1024,
        WriteBufferSize: 1024,
        CheckOrigin: func(r *http.Request) bool { return true }, // Allow all origins
    }
)

// Home renders the home page of the application
func Home(w http.ResponseWriter, r *http.Request) {
    log.Println("Rendering home page")
    err := renderPage(w, "home.html", nil)
    if err != nil {
        log.Println("Error rendering home page:", err)
    }
}

// WsJsonResponse represents the JSON structure for WebSocket responses
type WsJsonResponse struct {
    Action         string   `json:"action"`
    Message        string   `json:"message"`
    MessageType    string   `json:"message_type"`
    ConnectedUsers []string `json:"connected_users"`
}

// WsPayload represents the payload received from WebSocket clients
type WsPayload struct {
    Action   string          `json:"action"`
    Username string          `json:"username"`
    Message  string          `json:"message"`
    Conn     *websocket.Conn `json:"-"` // Exclude from JSON serialization
}

// WsEndpoint upgrades HTTP connections to WebSocket and initializes communication
func WsEndpoint(w http.ResponseWriter, r *http.Request) {
    log.Println("Client attempting to connect to WebSocket endpoint")

    ws, err := upgradeConnection.Upgrade(w, r, nil)
    if err != nil {
        log.Println("WebSocket upgrade failed:", err)
        return
    }

    log.Println("Client successfully connected to WebSocket endpoint")

    // Initial connection response
    response := WsJsonResponse{
        Message: "<em><small>Connected to server</small></em>",
    }

    clients[ws] = "" // Add the new connection to clients map

    err = ws.WriteJSON(response)
    if err != nil {
        log.Println("Error writing JSON response:", err)
        return
    }

    // Start listening for messages from this client
    go ListenForWs(ws)
}

// ListenToWsChannel listens for messages on the WebSocket channel and handles them
func ListenToWsChannel() {
    for {
        e := <-wsChannel
        var response WsJsonResponse

        switch e.Action {
        case "username":
            // Handle username assignment
            clients[e.Conn] = e.Username
            response.Action = "list_users"
            response.ConnectedUsers = getUserList()
            broadcastToAll(response)

        case "left":
            // Handle client disconnection
            response.Action = "list_users"
            delete(clients, e.Conn)
            response.ConnectedUsers = getUserList()
            broadcastToAll(response)

        case "broadcast":
            // Handle broadcast messages
            response.Action = "broadcast"
            response.Message = fmt.Sprintf("<strong>%s</strong>: %s", e.Username, e.Message)
            broadcastToAll(response)
        }
    }
}

// getUserList returns a sorted list of connected usernames
func getUserList() []string {
    var userList []string
    for _, username := range clients {
        if username != "" {
            userList = append(userList, username)
        }
    }
    sort.Strings(userList)
    return userList
}

// broadcastToAll sends a WebSocket response to all connected clients
func broadcastToAll(response WsJsonResponse) {
    for client := range clients {
        if err := client.WriteJSON(response); err != nil {
            log.Printf("Error sending message to client: %v", err)
            _ = client.Close()
            delete(clients, client)
        }
    }
}

// ListenForWs listens for messages from a specific WebSocket client
func ListenForWs(conn *websocket.Conn) {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("Recovered from error: %v", r)
        }
    }()

    var payload WsPayload
    for {
        if err := conn.ReadJSON(&payload); err != nil {
            log.Println("Error reading WebSocket message:", err)
            return
        }

        // Assign connection to payload and send it to the channel
        payload.Conn = conn
        wsChannel <- payload
    }
}

// renderPage renders a template with the given data
func renderPage(w http.ResponseWriter, tmpl string, data jet.VarMap) error {
    view, err := views.GetTemplate(tmpl)
    if err != nil {
        log.Println("Error loading template:", err)
        return err
    }

    if err := view.Execute(w, data, nil); err != nil {
        log.Println("Error executing template:", err)
        return err
    }
    return nil
}

Now that we’ve covered the core functionality in handlers.go, let’s look at how the routing and server setup are handled in routes.go and main.go. These files configure the application’s routes and initialize the server.

Application Routing

routes.go

package routes

import (
    "net/http"

    "github.com/arya2004/gobanter/pkg/handlers"
    "github.com/bmizerany/pat"
)


func Routes() http.Handler {
    mux := pat.New()


    mux.Get("/", http.HandlerFunc(handlers.Home))


    mux.Get("/ws", http.HandlerFunc(handlers.WsEndpoint))


    fileServer := http.FileServer(http.Dir("./assets/"))
    mux.Get("/assets/", http.StripPrefix("/assets/", fileServer))

    return mux
}

Explanation:

  1. Router Initialization:

    • pat.New() creates a new router using the pat package, which provides a simple pattern-based routing mechanism.
  2. Home Page Route:

    • The "/" route maps to the Home handler, which serves the home page.
  3. WebSocket Route:

    • The "/ws" route is connected to the WsEndpoint handler, establishing WebSocket communication.
  4. Static File Serving:

    • Serves static files (e.g., CSS, JS, images) from the assets directory using http.FileServer.

    • http.StripPrefix removes the /assets/ prefix from the URL path before serving files.

Application Entry Point

main.go

package main

import (
    "log"
    "net/http"

    "github.com/arya2004/gobanter/pkg/handlers"
    "github.com/arya2004/gobanter/pkg/routes"
)


func main() {

    mux := routes.Routes()


    log.Println("Starting WebSocket channel listener...")
    go handlers.ListenToWsChannel()


    log.Println("Server starting on port 8080...")
    if err := http.ListenAndServe(":8080", mux); err != nil {
        log.Fatalf("Error starting server: %v", err)
    }
}

Explanation:

  1. Initialize Routes:

    • The routes.Routes() function sets up all application routes and returns a http.Handler.
  2. WebSocket Channel Listener:

    • handlers.ListenToWsChannel() is launched in a goroutine to handle WebSocket messages asynchronously.
  3. Start HTTP Server:

    • The server listens on port 8080 using http.ListenAndServe with the router (mux) handling incoming requests.

    • Logs errors if the server fails to start.

User Interface for Chat Application

This HTML file defines the structure of the chat application's user interface. It includes the layout for sending messages, displaying the chat history, and listing online users. It uses Bootstrap for styling and incorporates custom CSS and JavaScript for functionality. The file is loaded as the home page of the application.

Copy and paste the above code into an home.html file in your project for the chat application interface.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Chat Application</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous" />

    <link rel="stylesheet" href="/assets/css/styles.css">
</head>

<body>
    <div class="container mt-5">
        <header class="text-center mb-4">
            <h1 class="display-4 fw-bold">Welcome to <span class="text-primary">GoBanter</span></h1>
            <p class="text-muted fs-5">Connect and communicate with ease</p>
            <hr class="mt-4">
        </header>
        <div class="row">
            <div class="col-md-8">
                <div class="card shadow">
                    <div class="card-header bg-primary text-white">
                        <div class="chat-header">Chat</div>
                    </div>
                    <div class="card-body">
                        <div class="mb-3">
                            <label for="username" class="form-label">Username</label>
                            <input type="text" id="username" class="form-control" placeholder="Enter your username">
                        </div>
                        <div class="mb-3">
                            <label for="message" class="form-label">Message</label>
                            <input type="text" id="message" class="form-control" placeholder="Type your message">
                        </div>
                        <div class="d-flex justify-content-between">
                            <button id="sendButton" class="btn btn-primary">Send Message</button>
                            <div id="status"></div>
                        </div>
                        <div id="output" class="chatbox mt-3"></div>
                    </div>
                </div>
            </div>
            <div class="col-md-4">
                <div class="card shadow">
                    <div class="card-header bg-secondary text-white">
                        <div class="chat-header">Who's Online</div>
                    </div>
                    <div class="card-body">
                        <ul id="online_users"></ul>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
        crossorigin="anonymous"></script>

    <script src="https://code.jquery.com/jquery-3.7.1.slim.min.js"
        integrity="sha256-kmHvs0B+OpCW5GVHUNjv9rOmY0IvSIRcf7zGUDTDQM8=" crossorigin="anonymous"></script>

    <script src="/assets/javascript/reconnecting-websocket.min.js"></script>

    <script src="/assets/javascript/main.js"></script>



</body>

</html>

Chat Application Styling

This CSS file defines the styles for the chat application, enhancing the layout and user interface elements. It includes styling for the chatbox, headers, and online user list. Use this file to ensure your application has a clean and responsive design.

Copy the code below and save it as styles.css:

.chatbox {
    border: 1px solid #ddd;
    border-radius: 0.5rem;
    min-height: 200px;
    max-height: 400px;
    overflow-y: auto;
    padding: 1em;
    background-color: #f8f9fa;
}

.chat-header {
    font-size: 1.5rem;
    font-weight: bold;
}

#online_users {
    list-style: none;
    padding: 0;
}

#online_users li {
    background: #e9ecef;
    margin-bottom: 0.5em;
    padding: 0.5em;
    border-radius: 0.5rem;
}

jQuery-Powered WebSocket Communication

This main.js file contains the client-side logic for interacting with the WebSocket server using jQuery. It includes functions to handle WebSocket events, update the UI dynamically, and manage user interactions. Below, we’ll break the code into sections and explain each part.

WebSocket Initialization

let socket = null;

$(document).ready(function () {
    const offline = `<span class="badge bg-danger">Not connected</span>`;
    const online = `<span class="badge bg-success">Connected</span>`;

    const $statusDiv = $("#status");
    const $output = $("#output");
    const $userField = $("#username");
    const $messageField = $("#message");
    const $onlineUsers = $("#online_users");

    socket = new ReconnectingWebSocket("ws://localhost:8080/ws", null, { debug: true, reconnectInterval: 3000 });

Explanation:

  • Variables:

    • offline and online: HTML badges indicating connection status.

    • $statusDiv, $output, $userField, $messageField, $onlineUsers: Cached jQuery selectors for UI elements.

  • WebSocket Connection:

    • ReconnectingWebSocket is used to establish a persistent WebSocket connection with auto-reconnect enabled.

Handling WebSocket Events

    socket.onopen = function () {
        console.log("connected!!");
        $statusDiv.html(online);
    };

    socket.onclose = function () {
        console.log("connection closed!");
        $statusDiv.html(offline);
    };

    socket.onerror = function () {
        console.log("there was an error");
        $statusDiv.html(offline);
    };

Explanation:

  • onopen: Updates the UI to show a "Connected" status when the WebSocket connection opens.

  • onclose: Changes the status to "Not connected" when the connection closes.

  • onerror: Handles connection errors by logging them and updating the UI.

Receiving WebSocket Messages

    socket.onmessage = function (msg) {
        const data = JSON.parse(msg.data);
        console.log("Action:", data.action);

        switch (data.action) {
            case "list_users":
                $onlineUsers.empty();
                if (data.connected_users.length > 0) {
                    $.each(data.connected_users, function (index, user) {
                        $onlineUsers.append(`<li class="list-group-item">${user}</li>`);
                    });
                }
                break;

            case "broadcast":
                $output.append(`<div>${data.message}</div>`);
                $output.scrollTop($output.prop("scrollHeight"));
                break;
        }
    };

Explanation:

  • onmessage:

    • Parses incoming WebSocket messages as JSON.

    • Switch Cases:

      • list_users: Updates the "Who's Online" list with connected usernames.

      • broadcast: Appends broadcast messages to the chatbox and scrolls to the latest message.

User Interaction: Username Update

    $userField.on("change", function () {
        const jsonData = {
            action: "username",
            username: $(this).val()
        };
        console.log(jsonData);
        socket.send(JSON.stringify(jsonData));
    });

Explanation:

  • Listens for changes in the username field.

  • Sends a username action to the server with the new username as JSON.

Sending Messages

    $messageField.on("keydown", function (event) {
        if (event.key === "Enter") {
            if (!socket || socket.readyState !== WebSocket.OPEN) {
                alert("No connection");
                return false;
            }

            if (!$userField.val() || !$messageField.val()) {
                alert("Enter username and message!");
                return false;
            } else {
                sendMessage();
            }

            event.preventDefault();
        }
    });

    $("#sendButton").on("click", function () {
        if (!$userField.val() || !$messageField.val()) {
            alert("Enter username and message!");
            return false;
        } else {
            sendMessage();
        }
    });
  • Enter Key:

    • Triggers sendMessage when Enter is pressed in the message field.

    • Validates that a username and message are provided and checks WebSocket connection status.

  • Send Button:

    • Performs the same validation and triggers sendMessage when the "Send Message" button is clicked.

Disconnecting on Page Unload

    $(window).on("beforeunload", function () {
        console.log("leaving ;(");
        if (socket && socket.readyState === WebSocket.OPEN) {
            const jsonData = { action: "left" };
            socket.send(JSON.stringify(jsonData));
        }
    });

Explanation:

  • Listens for the browser's beforeunload event (e.g., closing the tab or refreshing the page).

  • Sends a left action to notify the server that the user has disconnected.

Sending JSON Data to WebSocket

    function sendMessage() {
        const jsonData = {
            action: "broadcast",
            username: $userField.val(),
            message: $messageField.val()
        };
        socket.send(JSON.stringify(jsonData));
        $messageField.val("");
    }

Explanation:

  • Constructs a JSON object with the broadcast action, username, and message.

  • Sends the JSON to the server through the WebSocket connection.

  • Clears the message field after sending.

Full Code

Here’s the full main.js file without comments for you to copy and use:

let socket = null;

$(document).ready(function () {
    const offline = `<span class="badge bg-danger">Not connected</span>`;
    const online = `<span class="badge bg-success">Connected</span>`;

    const $statusDiv = $("#status");
    const $output = $("#output");
    const $userField = $("#username");
    const $messageField = $("#message");
    const $onlineUsers = $("#online_users");

    // Reconnecting WebSocket Initialization
    socket = new ReconnectingWebSocket("ws://localhost:8080/ws", null, { debug: true, reconnectInterval: 3000 });

    // WebSocket Events
    socket.onopen = function () {
        console.log("connected!!");
        $statusDiv.html(online);
    };

    socket.onclose = function () {
        console.log("connection closed!");
        $statusDiv.html(offline);
    };

    socket.onerror = function () {
        console.log("there was an error");
        $statusDiv.html(offline);
    };

    socket.onmessage = function (msg) {
        const data = JSON.parse(msg.data);
        console.log("Action:", data.action);

        switch (data.action) {
            case "list_users":
                $onlineUsers.empty();
                if (data.connected_users.length > 0) {
                    $.each(data.connected_users, function (index, user) {
                        $onlineUsers.append(`<li class="list-group-item">${user}</li>`);
                    });
                }
                break;

            case "broadcast":
                $output.append(`<div>${data.message}</div>`);
                $output.scrollTop($output.prop("scrollHeight"));
                break;
        }
    };

    // Username Field Change
    $userField.on("change", function () {
        const jsonData = {
            action: "username",
            username: $(this).val()
        };
        console.log(jsonData);
        socket.send(JSON.stringify(jsonData));
    });

    // Message Field Enter Key
    $messageField.on("keydown", function (event) {
        if (event.key === "Enter") {
            if (!socket || socket.readyState !== WebSocket.OPEN) {
                alert("No connection");
                return false;
            }

            if (!$userField.val() || !$messageField.val()) {
                alert("Enter username and message!");
                return false;
            } else {
                sendMessage();
            }

            event.preventDefault();
        }
    });

    // Send Button Click
    $("#sendButton").on("click", function () {
        if (!$userField.val() || !$messageField.val()) {
            alert("Enter username and message!");
            return false;
        } else {
            sendMessage();
        }
    });

    // WebSocket Disconnect on Page Unload
    $(window).on("beforeunload", function () {
        console.log("leaving ;(");
        if (socket && socket.readyState === WebSocket.OPEN) {
            const jsonData = { action: "left" };
            socket.send(JSON.stringify(jsonData));
        }
    });

    // Function to Send Message
    function sendMessage() {
        const jsonData = {
            action: "broadcast",
            username: $userField.val(),
            message: $messageField.val()
        };
        socket.send(JSON.stringify(jsonData));
        $messageField.val("");
    }
});

Conclusion

Building a real-time chat application using Go, Gorilla WebSocket, and jQuery is a great way to explore modern web development with WebSocket-based communication. By combining a robust backend in Go with jQuery on the frontend, you can achieve a seamless and interactive user experience.

The chat application features a user-friendly design as shown below:

This simple chat application demonstrates real-time user updates, message broadcasting, and client-server communication. To access the full source code and dive deeper into the implementation, visit the GitHub repository: https://github.com/arya2004/gobanter.

Happy coding!