WebSockets provide a full-duplex communication channel over a single TCP connection, enabling real-time communication between clients and servers. In this article, we’ll build an anonymous chat server in Golang using the net/http and golang.org/x/net/websocket packages, and we’ll style the chat interface using Tailwind CSS.

Prerequisites

Ensure you have Go installed on your system. You can download it from here. Also, make sure you have tailwindcss installed or you can link it via CDN.

Setting Up the Project

Let’s start by setting up the project structure and installing the necessary dependencies.

Create a new directory for your project and initialize it as a Go module:

mkdir chat
cd chat
go mod init chat

Next, create the following files within the project directory:

  1. main.go: This file will contain the Golang server code.
  2. index.html: This file will contain the HTML code for the chat interface.
  3. go.mod: This file specifies the module’s name and its dependencies.

main.go

package main

import (
  "fmt"
  "io"
  "net/http"

  "golang.org/x/net/websocket"
)

// Chat Server with connection pool
type Server struct {
  connections map[*websocket.Conn]string
}

// Create New Server which holds WebSocket connections
func NewServer() *Server {
  return &Server{
    connections: make(map[*websocket.Conn]string),
  }
}

// Listen on WebSocket's for messages
func (s *Server) listen(name string, chatWS *websocket.Conn) {
  // Buffer for fetching chat data
  buffer := make([]byte, 1024)

  for {
    // Read connection in buffer
    dataLength, err := chatWS.Read(buffer)

    if err != nil {
      // If WebSocket is terminated
      if err == io.EOF {
        break
      }

      // Log Read error and continue listening
      fmt.Println("Read Error:", err)
      continue
    }

    msg := string(buffer[:dataLength])
    fmt.Println(name+":", msg)

    // Broadcast message
    msgJson := "{\"name\": \"" + name + "\", \"message\": \"" + msg + "\"}"
    s.broadcast([]byte(msgJson))
  }
}

// Broadcast Message to all users/connections
func (s *Server) broadcast(data []byte) {
  // Loop all connections
  for ws, name := range s.connections {

    // Start new process to send message to connection
    go func(ws *websocket.Conn, name string) {
      if _, err := ws.Write(data); err != nil {
        fmt.Println("Write Error ("+name+"): ", err, " -> closing connection")

        // Terminate & Delete connection if closed
        ws.Close()
        delete(s.connections, ws)
      }
    }(ws, name)
  }
}

// Initiate Chat Websocket Connection
func (s *Server) handleChatWS(chatWS *websocket.Conn) {
  // Get Name of User from Parameters
  urlParams := chatWS.Request().URL.Query()
  name := urlParams.Get("name")

  fmt.Println("New Connection: ", name+" ("+chatWS.RemoteAddr().String()+")")

  // Add websocket connection to server pool
  s.connections[chatWS] = name

  // Start listening on socket
  s.listen(name, chatWS)
}

func main() {
  server := NewServer()

  // Load Chat Page
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    http.ServeFile(w, r, "index.html")
  })

  // Handle Chat WebSocket's
  http.Handle("/chatWS", websocket.Handler(server.handleChatWS))

  // Start Server
  fmt.Println("Server listening on :8000")
  http.ListenAndServe(":8000", nil)
}

Run go get which will load golang.org/x/net into the project go.mod.

index.html

<!doctype html>
<html>

<head>
  <title>Golang Tailwind Chat</title>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="https://cdn.tailwindcss.com"></script>
</head>

<body translate="no">
  <div class="max-w-screen-md mx-auto w-full flex flex-col h-screen max-h-screen overflow-hidden" id="chat">
    <div id="messagesDiv" class="messages flex-1 overflow-y-scroll border-box">

      <div class="inline-flex items-center justify-center w-full">
        <hr class="w-96 h-px my-8 border-0 bg-gray-300">
        <span id="chatTitle" class="absolute px-3 font-medium text-center -translate-x-1/2 left-1/2 text-grey bg-white">
          Golang Tailwind Chat
        </span>
      </div>
    </div>
    <div class="message-input bg-gray-100 p-4 flex flex-row">
      <input id="chatMsg" class="flex-1 p-2 rounded" type="text" placeholder="Write message and press enter" />
    </div>
  </div>

  <script>
    // DOM elements
    var msgField = document.getElementById("chatMsg");
    var messagesDiv = document.getElementById('messagesDiv');
    var chatTitle = document.getElementById('chatTitle');

    // Random Name List
    var nameList = [
      'Time', 'Past', 'Future', 'Dev',
      'Fly', 'Flying', 'Soar', 'Soaring', 'Power', 'Falling',
      'Fall', 'Jump', 'Cliff', 'Mountain', 'Rend', 'Red', 'Blue',
      'Green', 'Yellow', 'Gold', 'Demon', 'Demonic', 'Panda', 'Cat',
      'Kitty', 'Kitten', 'Zero', 'Memory', 'Trooper', 'XX', 'Bandit',
      'Fear', 'Light', 'Glow', 'Tread', 'Deep', 'Deeper', 'Deepest',
      'Mine', 'Your', 'Worst', 'Enemy', 'Hostile', 'Force', 'Video',
      'Game', 'Donkey', 'Mule', 'Colt', 'Cult', 'Cultist', 'Magnum',
      'Gun', 'Assault', 'Recon', 'Trap', 'Trapper', 'Redeem', 'Code',
      'Script', 'Writer', 'Near', 'Close', 'Open', 'Cube', 'Circle',
      'Geo', 'Genome', 'Germ', 'Spaz', 'Shot', 'Echo', 'Beta', 'Alpha',
      'Gamma', 'Omega', 'Seal', 'Squid', 'Money', 'Cash', 'Lord', 'King',
      'Duke', 'Rest', 'Fire', 'Flame', 'Morrow', 'Break', 'Breaker', 'Numb',
      'Ice', 'Cold', 'Rotten', 'Sick', 'Sickly', 'Janitor', 'Camel', 'Rooster',
      'Sand', 'Desert', 'Dessert', 'Hurdle', 'Racer', 'Eraser', 'Erase', 'Big',
      'Small', 'Short', 'Tall', 'Sith', 'Bounty', 'Hunter', 'Cracked', 'Broken',
      'Sad', 'Happy', 'Joy', 'Joyful', 'Crimson', 'Destiny', 'Deceit', 'Lies',
      'Lie', 'Honest', 'Destined', 'Bloxxer', 'Hawk', 'Eagle', 'Hawker', 'Walker',
      'Zombie', 'Sarge', 'Capt', 'Captain', 'Punch', 'One', 'Two', 'Uno', 'Slice',
      'Slash', 'Melt', 'Melted', 'Melting', 'Fell', 'Wolf', 'Hound',
      'Legacy', 'Sharp', 'Dead', 'Mew', 'Chuckle', 'Bubba', 'Bubble', 'Sandwich',
      'Smasher', 'Extreme', 'Multi', 'Universe', 'Ultimate', 'Death', 'Ready', 'Monkey',
      'Elevator', 'Wrench', 'Grease', 'Head', 'Theme', 'Grand', 'Cool', 'Kid', 'Boy', 'Girl',
      'Vortex', 'Paradox'
    ];

    // Get Random Name for chat
    const name = nameList[Math.floor(Math.random() * nameList.length)];

    // Update Chat Title for name
    chatTitle.innerHTML = "Golang Tailwind Chat (" + name + ")";
    document.title = "Chat: " + name;

    // Initiate WebSocket
    let socket = new WebSocket("ws://localhost:8000/chatWS?" + new URLSearchParams({ name: name }));

    console.log("Connected to Chat WebSocket as", name);

    socket.onmessage = (event) => {
      console.log("Received: ", event.data);

      data = JSON.parse(event.data);

      // if message is from sender
      if (data.name == name) {
        messagesDiv.innerHTML += '<div class="message-row flex flex-row-reverse text-white">\
          <div class="message m-2 p-4 bg-pink-500 rounded max-w-full inline relative shadow">\
            <p class="message-content">'+ data.message + '</p>\
          </div>\
        </div>';
      } else {
        messagesDiv.innerHTML += '<div class="message-row flex flex-row">\
          <div class="message m-2 p-4 pb-8 bg-gray-100 rounded max-w-full inline relative shadow">\
            <p class="message-content">'+ data.message + '</p>\
            <div class="message-name absolute bg-gray-300 px-2 py-1 text-xs rounded-bl rounded-tr left-0 bottom-0">'+ data.name + '</div>\
          </div>\
        </div>';
      }

      // Scroll to bottom
      messagesDiv.scrollTop = messagesDiv.scrollHeight;
    }

    // Add event listener for keypress to get chat message
    msgField.addEventListener("keypress", function (event) {
      // Check if the Enter key is pressed
      if (event.keyCode === 13) {
        // Retrieve the value of the input field
        var msg = msgField.value;

        // Send message to socket
        socket.send(msg);

        // Clear the input field (optional)
        msgField.value = "";
      }
    });
  </script>
</body>

</html>

Running the Server

To run the server, execute the following command in your terminal:

go run .

This will start the server on port 8000.

Output:

$ go run .
Server listening on :8000
New Connection:  Camel (http://localhost:8000)
New Connection:  Duke (http://localhost:8000)
Camel: Hello there, I am Camel
Duke: Hi, I am Duke
Camel: Duke, Isn't Golang fun ?
Duke: Sure it is!
Duke: And Tailwind too

Accessing the Chat Interface

Open your web browser and navigate to http://localhost:8000. You should see the chat interface where you can enter messages and see them displayed in real-time.

regular

Conclusion

In this article, we’ve built an anonymous chat server in Golang using WebSockets and styled the chat interface using Tailwind CSS. WebSockets provide a powerful mechanism for real-time communication between clients and servers, making them ideal for building chat applications and other real-time systems.

Happy coding! 🚀

References