This project is a real-time chat application utilizing a Go backend with WebSocket technology for handling communication and a React frontend for the user interface. The Go server efficiently manages WebSocket connections, enabling instant message broadcasting between connected clients. The React frontend offers a responsive chat interface, allowing users to send and receive messages in real-time. Together, these technologies create a scalable and high-performance chat system, demonstrating how modern web applications can leverage WebSocket for seamless real-time communication.
Introduction
In this article, we will walk through the development of a real-time chat room application using Go for the backend (WebSocket server) and React for the frontend. This project highlights how modern web applications can use WebSocket for instant, real-time communication.
Project Overview
The chat application allows users to connect and send messages in real-time. When a user sends a message, it is broadcast to all connected clients without the need to refresh the page. We use Go for handling WebSocket connections on the server side, and React for rendering the UI on the client side.
Project Structure
The project consists of two parts:
- Go backend: A WebSocket server that handles real-time communication.
- React frontend: A web-based user interface for sending and receiving messages
Here is an overview of the project structure:
├── real-time-chat/ # Go WebSocket server (backend)
├── chatroom/ # React chat application (frontend)
└── README.md # Project documentation
The Go WebSocket Server (Backend)
Why Go?
Go (or Golang) is a great language for building high-performance network applications due to its concurrency model and efficient handling of I/O operations. For this project, Go's net/http package and gorilla/websocket package provide an efficient way to handle WebSocket connections, ensuring messages are exchanged in real time.
Prerequisites
1. Install VS Code
If haven't already, download and install VS Code from the official website.
2. Install Go
Make sure Go is installed on your system. Download it from the Go website.
After installation, confirm Go is set up correctly by running:
go version
Ensure $GOPATH
and $GOROOT
are properly configured in environment variables.
3. Install the Go Extension for VS Code
- Open VS Code.
- Go to the Extensions panel (on the left sidebar or press
Ctrl+Shift+X
). - Search for Go (by the Go team at Google) and install it.
This extension will provide linting, auto-formatting, IntelliSense, and other development tools.
4. Set Up Go Tools
Once the Go extension is installed, VS Code will prompt you to install some additional Go tools (like gopls
, gofmt
, delve
for debugging, etc.). These tools enhance your development experience.
When prompted to install tools, click Install All, or can install them manually by running:
go install golang.org/x/tools/gopls@latest
go install golang.org/x/lint/golint@latest
go install github.com/go-delve/delve/cmd/dlv@latest
go install golang.org/x/tools/cmd/goimports@latest
5. Go Modules Support
If using Go modules (for dependency management), make sure you're in a Go module project. Initialize a new module with:
go mod init project-name
Setting Up the Backend
Step 1: Create the Go WebSocket Server
We start by creating the Go backend, which listens for WebSocket connections, manages active clients, and broadcasts messages between them.
Here’s a simplified breakdown of the key components of the Go backend:
- WebSocket Connections: The server establishes WebSocket connections with clients.
- Client Management: The server keeps track of connected clients using a map.
- Broadcasting Messages: When one client sends a message, the server broadcasts it to all other clients in real time.
Step 2: Implementing the Go WebSocket Server
- Create a Project Directory
First, create a folder for chat application.
mkdir real-time-chat
cd real-time-chat
To make use of Go modules for dependency management, initialize project with go mod
:
go mod init real-time-chat
- Organize Project Structure
real-time-chat/
│
├── go.mod # For dependency management
├── main.go # Entry point of your app
├── handlers/
│ └── websocket.go # WebSocket-related logic
└── public/
└── index.html # Frontend (optional: for testing)
- Create
main.go
for the Application Entry Point
main.go
file will serve as the entry point to the application. This file will set up the server and handle incoming connections.
Here’s main.go
file:
package main
import (
"log"
"net/http"
"real-time-chat/handlers"
)
func main() {
fs := http.FileServer(http.Dir("./public"))
http.Handle("/", fs)
http.HandleFunc("/ws", handlers.HandleConnections)
log.Println("Server started on :8080")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
Declares the package name as main
, meaning this is the entry point of the application. Every Go application that run starts with the main
package.
We import:
- log: Used to log server events (e.g., starting the server, errors).
- net/http: Provides HTTP functionalities to create a web server and handle requests.
- real-time-chat/handlers: A custom package where the WebSocket connection logic is defined. This is the file we discussed earlier (
handlers/websocket.go
). http.FileServer
: Serves static files (like HTML, CSS, and JS). Here, it's serving files from the public/
directory. http.Handle("/", fs)
: Routes all requests to the root URL (/
) to the file server, so when users visit http://localhost:8080
, they will see the static index.html
file from the public/
folder.
- Create a
handlers/websocket.go
File for WebSocket Logic
Separate the WebSocket logic into a new websocket.go
file under a handlers/
folder. This will keep code more modular and organized.
websocket.go
file:
package handlers
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
var clients = make(map[*websocket.Conn]bool)
var broadcast = make(chan Message)
type Message struct {
Username string `json:"username"`
Message string `json:"message"`
Timestamp string `json:"timestamp"`
Typing bool `json:"typing"`
}
func HandleConnections(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Fatal(err)
}
defer ws.Close()
clients[ws] = true
for {
var msg Message
err := ws.ReadJSON(&msg)
if err != nil {
log.Printf("error: %v", err)
delete(clients, ws)
break
}
broadcast <- msg
}
}
func HandleMessages() {
log.Println("HandleMessages running")
for {
msg := <-broadcast
for client := range clients {
err := client.WriteJSON(msg)
if err != nil {
log.Printf("error: %v", err)
client.Close()
delete(clients, client)
}
}
}
}
http.HandleFunc
: Registers a new route for the WebSocket connection at /ws
. When a client connects to ws://localhost:8080/ws
, this route handles it. handlers.HandleConnections
: This function (defined in websocket.go
) handles the WebSocket connection for each client. It upgrades the HTTP connection to a WebSocket connection. log.Println("Server started on :8080")
: Logs a message to indicate the server is up and running. http.ListenAndServe(":8080", nil)
: Starts the HTTP server on port 8080. The first argument (:8080
) specifies the address (in this case, port 8080), and the second argument (nil
) means it will use the default ServeMux
to handle routes.
- Add a Frontend for Testing
For testing the WebSocket functionality, just add an index.html
file inside the public/
folder with basic HTML/JS to connect to the WebSocket.
Here’s index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Real-Time Chat</title>
</head>
<body>
<h2>WebSocket Chat</h2>
<div id="messages"></div>
<input id="username" type="text" placeholder="Username" />
<input id="message" type="text" placeholder="Message" />
<button onclick="sendMessage()">Send</button>
<script>
const socket = new WebSocket('ws://localhost:8080/ws');
socket.onmessage = function(event) {
const messages = document.getElementById('messages');
const message = document.createElement('div');
message.textContent = event.data;
messages.appendChild(message);
};
function sendMessage() {
const username = document.getElementById('username').value;
const message = document.getElementById('message').value;
socket.send(JSON.stringify({username: username, message: message}));
}
</script>
</body>
</html>
Running the Go Backend
To run the Go server:
cd real-time-chat/
go run main.go
The server will now listen for WebSocket connections on ws://localhost:8080/ws.
Then go http://localhost:8080
The React Frontend
Why React?
React is a popular JavaScript library for building user interfaces. Its component-based architecture allows for efficient UI updates, making it perfect for real-time applications where messages need to be displayed instantly upon receipt.
Setting Up the Frontend
Step 1: Create a New React App
Using create-react-app (with TypeScript), we created the frontend. The frontend connects to the WebSocket server and listens for messages to be displayed in real-time.
npx create-react-app chatroom --template typescript
cd chatroom
Step 2: Add Emoji Picker to the Chat
npm install emoji-mart
Step 3: Create ChatRoom Component
Here’s a breakdown of the React components:
WebSocket Connection: The client connects to the Go WebSocket server.
State Management: The app uses React’s useState and useEffect hooks to manage messages and WebSocket connections.
Real-Time Updates: When the server broadcasts a message, the frontend immediately displays it.
Inside the src/
folder, create a new file called ChatRoom.tsx
.
Here's ChatRoom.tsx
import React, { useState, useEffect, useRef, ChangeEvent, KeyboardEvent } from "react";
import Picker from '@emoji-mart/react';
import data from '@emoji-mart/data';
interface Message {
username: string;
message: string;
timestamp: string;
typing: boolean;
}
const ChatRoom: React.FC = () => {
const [username, setUsername] = useState<string>("");
const [message, setMessage] = useState<string>("");
const [chat, setChat] = useState<Message[]>([]);
const [typingUser, setTypingUser] = useState<string | null>(null);
const [ws, setWs] = useState<WebSocket | null>(null);
const [showPicker, setShowPicker] = useState<boolean>(false);
const messageRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const socket = new WebSocket("ws://localhost:8080/ws");
socket.onmessage = (event) => {
const messageData: Message = JSON.parse(event.data);
if (messageData.typing && messageData.username !== username) {
setTypingUser(messageData.username);
} else if (!messageData.typing) {
setChat((prevChat) => [...prevChat, messageData]);
setTypingUser(null);
}
};
setWs(socket);
return () => {
socket.close();
};
}, [username]);
const sendMessage = () => {
if (ws && message && username) {
const timestamp = new Date().toLocaleTimeString();
const msg: Message = { username, message, timestamp, typing: false };
ws.send(JSON.stringify(msg));
setMessage("");
}
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
sendMessage();
}
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setMessage(e.target.value);
if (ws && username) {
const typingMessage: Message = { username, message: "", timestamp: "", typing: true };
ws.send(JSON.stringify(typingMessage));
}
};
const addEmoji = (emoji: any) => {
setMessage((prevMessage) => prevMessage + emoji.native);
setShowPicker(false);
};
return (
<div className="chatroom-container">
<div className="chatbox">
<h2>Chat Room</h2>
<div className="chat-inputs">
<input
type="text"
placeholder="Enter your username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="chat-window">
{typingUser && <div className="typing-indicator">{typingUser} is typing...</div>}
{chat.map((msg, index) => (
<div
key={index}
className={`chat-message ${msg.username === username ? "own-message" : ""}`}
>
<div className="chat-message-info">
<img
src={`https://avatars.dicebear.com/api/initials/${msg.username}.svg`}
alt="avatar"
className="chat-avatar"
/>
<strong className="username">{msg.username}</strong>
<span className="timestamp"> at {msg.timestamp}</span>
</div>
<div>{msg.message}</div>
</div>
))}
</div>
<div className="chat-inputs">
<input
ref={messageRef}
type="text"
placeholder="Enter your message"
value={message}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
<button onClick={sendMessage}>Send</button>
<button onClick={() => setShowPicker(!showPicker)}>😊</button>
{showPicker && <Picker data={data} onEmojiSelect={addEmoji} />}
</div>
</div>
</div>
);
};
export default ChatRoom;
-
WebSocket Connection:
- When the component mounts (
useEffect
), a WebSocket connection is established to Go backend at ws://localhost:8080/ws
. - The WebSocket listens for incoming messages (
socket.onmessage
) and updates the chat history. - The WebSocket connection is closed when the component unmounts to clean up resources.
-
Sending Messages:
- The
sendMessage
function sends the user’s message and username as a JSON object through the WebSocket. - The
Enter
key press is captured to send the message when the user presses the Enter key.
-
Displaying Chat:
- The
chat
state stores the entire chat history. - Each new message is appended to
chat
and rendered in the chat window.
Step 4: Add ChatRoom Component to App.tsx
import ChatRoom from "./ChatRoom";
import './App.css';
function App() {
return (
<div className="App">
<ChatRoom />
</div>
);
}
export default App;
Step 4: Add CSS Styling
Add styling for the chatroom in App.css
.chatroom-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f9f9f9;
}
.chatbox {
width: 500px;
background-color: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
font-size: 1.5em;
margin-bottom: 20px;
}
.chat-inputs {
margin-bottom: 10px;
display: flex;
justify-content: space-between;
}
.chat-inputs input[type="text"] {
flex-grow: 1;
padding: 10px;
border-radius: 5px;
border: 1px solid #ccc;
margin-right: 10px;
}
.chat-inputs button {
padding: 10px 15px;
border: none;
background-color: #007bff;
color: #fff;
border-radius: 5px;
cursor: pointer;
}
.chat-inputs button:hover {
background-color: #0056b3;
}
.chat-inputs button:nth-child(3) {
background-color: #ffcc00;
}
.chat-inputs button:nth-child(3):hover {
background-color: #e6b800;
}
.chat-window {
height: 300px;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
overflow-y: auto;
margin-bottom: 10px;
}
.chat-message {
display: flex;
flex-direction: column;
padding: 10px;
border-radius: 8px;
margin-bottom: 10px;
}
.own-message {
background-color: #d1ffd1;
align-self: flex-end;
}
.chat-message-info {
display: flex;
align-items: center;
}
.username {
margin-right: 5px;
}
.timestamp {
margin-left: 5px;
}
.chat-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
}
.typing-indicator {
font-style: italic;
color: gray;
}
button {
cursor: pointer;
}
Running the React Frontend
To start the React frontend:
cd chatroom/
npm start
The frontend will be available at http://localhost:3000.
Real-Time Messaging with WebSocket
When a message is sent from the frontend, it is transmitted via WebSocket to the Go server, which then broadcasts the message to all connected clients. WebSocket enable full-duplex communication, ensuring that messages are received instantly, creating a seamless real-time chat experience.
Example Workflow
- User A types a message in the chatbox and presses "Send".
- The message is sent to the Go server over a WebSocket connection.
- The server broadcasts the message to all connected clients (including User A).
- All clients update their chat windows with the new message in real time.
Conclusion
In this article, we walked through how to build a simple real-time chat application using Go for the backend and React for the frontend. The application leverages WebSocket to enable real-time communication between clients. Both Go and React are powerful technologies that can be used to build scalable, high-performance applications.