๐ฌ Building a Real-Time WebSocket Broadcast Server in Go

In this post, Iโll walk you through how I built a real-time WebSocket broadcast server and client in Go โ a simple chat system that lets multiple clients connect and exchange messages instantly from the terminal.
This project combines:
๐ง Goroutines for concurrency
๐ Channels for message communication
๐ Mutexes for safe shared access
๐ Gorilla WebSocket for real-time communication
๐ป Cobra CLI for a clean command-line interface
๐ Project idea from:
๐ https://roadmap.sh/projects/broadcast-server
๐ Repo:
๐ https://github.com/pranav767/GO_PROJECTS/tree/main/broadcast-server
โ๏ธ What the Project Does
This project acts as both a server and a client โ depending on which command you run.
The server accepts WebSocket connections and broadcasts messages to all connected clients.
The clients connect from the terminal and send/receive messages in real-time.
You can:
Chat with multiple users via terminal
See join/leave system notifications
Change your display name using
/name NewNameQuit with
/quit
๐งฑ Project Structure
broadcast-server/
โโโ cmd/
โ โโโ connect.go # CLI for client
โ โโโ root.go # Root Cobra command
โ โโโ start.go # CLI for server
โโโ client/
โ โโโ client.go # WebSocket client logic
โโโ server/
โ โโโ server.go # WebSocket server logic
โโโ main.go # Entry point
โโโ go.mod # Dependencies
๐ง Code Flow (High-Level)
The server starts and listens for
/wsconnections.Each connected client runs in its own goroutine.
When a client sends a message:
- Itโs placed on a channel (
broadcast).
- Itโs placed on a channel (
A separate goroutine (
handleMessages) listens to that channel.The message is broadcasted to all clients concurrently.
Mutex locks protect shared data (
clientsmap) during writes/removals.
What are Web Sockets?
A WebSocket is a communication Protocol providing full duplex, real-time communication between a server & client over a single persistent TCP Connection. After an initial HTTP handshake, the connection is upgraded to a WebSocket , allowing both the client & server to send messages independently & simultaneously.
What are Golang Routines
A GoRoutine is a lightweight thread of execution. The way we normally use functions is
package main
import (
"fmt"
"time"
)
func f(from string) {
for i := range 3 {
fmt.Println(from, ":", i)
}
}
func main() {
f("direct")
func (msg string) {
fmt.Println(msg)
}("going")
}
// this will print
// direct : 1
// direct : 2
// direct : 3
// Going
This is the traditional way we execute/write functions, if you look at the function flow, when it starts with main function, it executes f(โdirectโ) & will wait untill this is finished, after finishing the execution of this function it goes with the execution of next anoymous function, but what if we donโt want to wait for the first function to complete and keep
It allows you to execute functions concurrently
๐งฉ main.go
Entry point for the application
// Package main is the entry point for the broadcast server application.
package main
import "broadcast-server/cmd"
func main() {
cmd.Execute()
}
Explanation:
Imports the
cmdpackage (which contains all command definitions).Calls
cmd.Execute()โ Cobraโs entry point for parsing commands.Based on the subcommand (
startorconnect), it routes execution accordingly.Keeps
main.gominimal โ this is a Go best practice.
๐ก Your binary can act as both server and client, depending on which command you run.
โ๏ธ cmd/root.go
Initializes the Cobra command-line interface.
// Package cmd provides the command-line interface for the broadcast server application.
package cmd
import (
"os"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "broadcast-server",
Short: "A WebSocket broadcast server and client",
Long: `A real-time chat application using WebSocket technology.
This application can be run either as a server to accept connections
or as a client to connect to an existing server.`,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
Cobra provides structured subcommands like:
broadcast-server startbroadcast-server connect
Handles flag parsing, validation, and help text.
Useโ defines how to run the command (broadcast-server).Shortโ one-line summary used in help.Longโ multi-line description printed in CLI help output.rootCmdacts as the parent tostartandconnectcommands.
Execute()runs the CLI and parses arguments.If user runs an invalid command or thereโs a parsing error, it exits cleanly.
๐ก Cobraโs design helps you keep your CLI code structured and scalable.
๐งญ cmd/start.go
Defines the server command
package cmd
import (
"broadcast-server/server"
"github.com/spf13/cobra"
)
// startCmd represents the start command
var startCmd = &cobra.Command{
Use: "start",
Short: "Start the WebSocket broadcast server",
Long: `Start the WebSocket broadcast server and begin accepting client connections.
The server will listen for incoming WebSocket connections and broadcast
messages between all connected clients.`,
Run: func(cmd *cobra.Command, args []string) {
host, _ := cmd.Flags().GetString("host")
port, _ := cmd.Flags().GetInt("port")
server.StartServer(host, port)
},
}
func init() {
rootCmd.AddCommand(startCmd)
startCmd.Flags().StringP("host", "H", "localhost", "Host address to bind to")
startCmd.Flags().IntP("port", "p", 8080, "Port to listen on")
}
Useโ command name (start).Short&Longโ describe what this command does.Runโ main function executed when this command is called.
Inside the Run function:
Reads CLI flags:
--host(default:localhost)--port(default:8080)
Passes them to
server.StartServer(host, port).This call starts the WebSocket server and begins listening for connections.
Adds
--hostand--portflags with short forms (-H,-p).Cobra automatically includes these in help output.
๐ก Running go run main.go start -H 0.0.0.0 -p 9000 binds the server to port 9000.
๐ cmd/connect.go
Defines the client command
package cmd
import (
"broadcast-server/client"
"fmt"
"github.com/spf13/cobra"
)
var connectCmd = &cobra.Command{
Use: "connect",
Short: "Connect to the broadcast server",
Long: `Connect to the WebSocket broadcast server as a client`,
Run: func(cmd *cobra.Command, args []string) {
host, _ := cmd.Flags().GetString("host")
port, _ := cmd.Flags().GetInt("port")
name, _ := cmd.Flags().GetString("name")
fmt.Printf("Connecting to %s:%d as %s\n", host, port, name)
client.StartClient(host, port, name)
},
}
func init() {
rootCmd.AddCommand(connectCmd)
connectCmd.Flags().StringP("host", "H", "localhost", "Server host address")
connectCmd.Flags().IntP("port", "p", 8080, "Server port")
connectCmd.Flags().StringP("name", "n", "anonymous", "Client name")
}
Useโ definesconnectcommand.Short&Longโ describe what this command does.Runโ executes when the user runsbroadcast-server connect.
Inside Run:
Reads CLI flags:
--hostor-H: WebSocket server host (default:localhost)--portor-p: Port to connect (default:8080)--nameor-n: Username for the client (default:anonymous)
Prints the connection details for user feedback.
Calls
client.StartClient(host, port, name)to initialize the WebSocket client logic.
๐ก Each terminal acts as one chat client.
Registers flags for host, port, and name.
Automatically integrates with Cobraโs help system.
๐ก Running go run main.go connect -n Alice opens a chat session as Alice.
๐งฎ Server Code (server/server.go)
The WebSocket broadcast server logic.
๐งฑ Key Variables
var clients = make(map[*Client]bool) // Track all clients
var mu sync.Mutex // Protect shared access
var broadcast = make(chan []byte) // Message channel
clientsโ map of all connected clients.muโ ensures thread-safe read/write access.broadcastโ channel to send messages to all clients.
๐ฅ handleConnection()
Handles new WebSocket client connections.
func handleConnection(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
conn, _ := upgrader.Upgrade(w, r, nil)
client := &Client{conn: conn, name: name}
mu.Lock()
clients[client] = true
broadcast <- []byte(fmt.Sprintf("System: %s joined", client.name))
mu.Unlock()
for {
_, msg, err := client.conn.ReadMessage()
if err != nil {
mu.Lock()
delete(clients, client)
broadcast <- []byte(fmt.Sprintf("System: %s left", client.name))
mu.Unlock()
break
}
if len(msg) > 6 && string(msg[:6]) == "/name " {
old := client.name
client.name = string(msg[6:])
broadcast <- []byte(fmt.Sprintf("System: %s is now known as %s", old, client.name))
continue
}
broadcast <- []byte(fmt.Sprintf("%s: %s", client.name, string(msg)))
}
}
What happens here:
Converts HTTP โ WebSocket connection using Gorilla WebSocket.
Extracts the
namequery parameter (?name=Alice).Creates a new
Clientstruct with the connection and name.Adds client to active list (
clientsmap).Locks the mutex before modifying shared
clientsmap.Adds client to map and broadcasts join message.
Unlocks mutex after operation.
Then enters an infinite loop to listen for messages:
Waits for messages from client.
If connection closes or error occurs โ removes client safely and announces departure.
Handles /name command:
Broadcasts โjoinedโ message.
Waits for messages:
If
/name, updates user name.Otherwise sends message to broadcast channel.
On disconnect:
Removes client safely using mutex.
Broadcasts โleft the chatโ.
Upgrades the HTTP connection to a WebSocket connection using Gorilla WebSocket.
๐ก Each client connection runs in its own goroutine, so multiple users can chat concurrently.
๐ค handleMessages()
Broadcast messages to all connected clients.
func handleMessages() {
for {
msg := <-broadcast
mu.Lock()
for client := range clients {
client.conn.WriteMessage(websocket.TextMessage, msg)
}
mu.Unlock()
}
}
Continuously reads messages from the
broadcastchannel.Locks
clientsto safely iterate over all connected users.Sends (
WriteMessage) the message to each clientโs WebSocket.Removes any disconnected client.
๐ก This function is the broadcaster loop โ every message from any user flows through here.
๐ StartServer()
func StartServer(host string, port int) {
http.HandleFunc("/ws", handleConnection)
go handleMessages()
addr := fmt.Sprintf("%s:%d", host, port)
fmt.Printf("Server started at ws://%s/ws\n", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}
Explanation:
Registers
/wsendpoint.Launches broadcaster (
handleMessages()) as goroutine.Starts HTTP server to listen for WebSocket connections.
๐ก Server ready to broadcast!
๐ป Client Code (client/client.go)
The WebSocket client logic that connects to the server and interacts in real time.
๐ StartClient()
addr := fmt.Sprintf("ws://%s:%d/ws?name=%s", host, port, name)
conn, _, err := websocket.DefaultDialer.Dial(addr, nil)
defer conn.Close()
Builds WebSocket URL (e.g.,
ws://localhost:8080/ws?name=Alice).Connects using Gorilla WebSocket Dialer.
Closes connection when finished.
๐ง Message Receiver (Goroutine)
go func() {
for {
_, msg, err := conn.ReadMessage()
if err != nil {
log.Println("read error: ", err)
return
}
fmt.Printf("\n[Broadcast] %s\n>", string(msg))
}
}()
Runs in a separate goroutine for asynchronous reading.
Constantly listens for messages from the server.
Prints every broadcast message prefixed with
[Broadcast].
โจ๏ธ User Input Loop
scanner := bufio.NewScanner(os.Stdin)
fmt.Print(">")
for scanner.Scan() {
text := scanner.Text()
if text == "/quit" {
fmt.Println("Exiting...")
return
}
conn.WriteMessage(websocket.TextMessage, []byte(text))
fmt.Print(">")
}
Reads user input line by line using
bufio.Scanner.If
/quit, exits cleanly.Otherwise, sends message as WebSocket text frame to server.
๐ก This loop runs concurrently with the receiver goroutine, allowing full-duplex communication.
๐ฆ Libraries Used
| Library | Purpose |
| github.com/gorilla/websocket | Real-time WebSocket connections |
| github.com/spf13/cobra | Command-line interface |
| sync, net/http, fmt, bufio, os | Standard Go libraries |
๐ Goroutines
Lightweight threads managed by Goโs runtime.
Run concurrently using
gokeyword:go handleMessages()Here used for:
Each client connection (
handleConnection).Message broadcaster (
handleMessages).Client reader goroutine.
๐ก Goroutines make the chat system concurrent and non-blocking.
๐ฌ Golang Channels
Channels provide safe communication between goroutines.
Created using:
broadcast := make(chan []byte)Used for:
Sending messages from clients to broadcaster:
broadcast <- []byte("Alice: Hello")Receiving messages to forward to all clients:
msg := <-broadcast
๐ก Channels are like Goโs internal message queues.
๐ Golang Mutex
Ensures safe access to shared variables between goroutines.
Used when updating the
clientsmap:mu.Lock() clients[client] = true mu.Unlock()Prevents race conditions (when two goroutines modify data simultaneously).
๐ก Mutex = mutual exclusion. Only one goroutine modifies data at a time.
๐ Gorilla WebSocket
Popular Go library implementing the WebSocket protocol.
Enables real-time, bi-directional communication over TCP.
In server:
upgrader := websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } conn, _ := upgrader.Upgrade(w, r, nil)In client:
conn, _, _ := websocket.DefaultDialer.Dial(addr, nil)
๐ก Turns HTTP into persistent, full-duplex WebSocket connections.
๐ How to Run
Start the Server:
go run main.go start
Connect Clients:
go run main.go connect --name "Alice"
go run main.go connect --name "Bob"
Now you have a real-time terminal chat ๐
โ๏ธ Example Output
Server:
Server started at ws://localhost:8080/ws
Broadcasting: System: Alice joined
Broadcasting: Alice: Hello everyone
Client 1 (Alice):
>Hello everyone
[Broadcast] Bob: Hi Alice!
Client 2 (Bob):
[Broadcast] System: Alice joined the chat
Hi Alice!
๐งฐ Key Learnings
How WebSockets enable real-time communication
โ
How goroutines enable parallelism
โ
How channels pass messages between goroutines
โ
How mutexes keep shared state consistent
โ
How to build a CLI with Cobra
This project shows how Goโs concurrency features work seamlessly with WebSockets โ perfect for chat apps, live dashboards, or multiplayer systems.
This is it for websockets.


