Simple Go Chat Application in under 100 lines of code - Part 1
Tags: go, websockets, webdev, systemdesign
Published: 2024-05-04 08:39:35

_This blog is the first part of a multi part series._
- _**Simple Go Chat Application in under 100 lines of code - Part 1**_
- _[Simple Go Chat Application in under 100 lines of code - Part 2](https://dev.to/gopher-wizards/simple-go-chat-application-in-under-100-lines-of-code-part-2-1g67)_


Yes you read it right! This blog provides a comprehensive guide on creating a straightforward broadcast chat application in under 100 lines of Golang code.

The blog is structured into two parts.

The initial segment delves into the implementation of a chat application using the Gorilla websockets package in Golang, accompanied by a basic static HTML page for the chat interface. In the subsequent section, we will explore enhancing the scalability of the application by incorporating Redis pub-sub.

Why Websockets over HTTP ?

- Websockets offer bidirectional, low-latency communication, ideal for real-time applications, unlike HTTP's request-response nature.
- This choice eliminates the need for polling for new messages from the user interface.

Let’s dive into how we can implement a websocket based chat application in Golang.

Let’s start by creating a directory called `go_chat` and initializing a new go module.

```zsh
mkdir go_chat
cd go_chat
go mod init go_chat
```

Let’s install the dependencies using go get

```shell
go get github.com/gin-gonic/gin
go get github.com/gorilla/websocket
```

Let's write a simple main.go file with a main() function that initializes a Gin server.

```go
package main

import (
	"log"
	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.Default()
	err := router.Run()
	if err != nil {
		log.Fatalf("Unable to start server. Error %v", err)
	}
	log.Println("Server started successfully.")
}

```

Next, let’s write a gin handler to handle websocket connections

```go
func serveWs(c *gin.Context) {

	upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		log.Printf("Error in upgrading web socket. Error: %v", err)
		return
	}

	go handleClient(conn)
}
```

Let's break down the serveWs function.

- Websockets start with an HTTP handshake, then upgrade to a persistent TCP connection for full-duplex communication. In order to do this, we need to upgrade the HTTP connection using websocket.Upgrader from the gorilla websockets package.
- The websocket.Upgrader accepts a CheckOrigin function to handle CORS.
Note: For demonstration purposes, we have implemented the CheckOrigin function to always return true. (Refrain from implementing this in a production setting unless you're willing to expose yourself to potential CSRF attacks).
- The Upgrade function takes in an http.ResponseWriter and an http.Request, and returns a websocket.Conn and an error if there is one.

Now that we have the websocket connection, let’s write a goroutine handles the connection.
```go
var clients = make(map[*websocket.Conn]struct{})

type Message struct {
	From    string `json:"from"`
	Message string `json:"message"`
}

func handleClient(c *websocket.Conn) {
	defer func() {
		delete(clients, c)
		log.Println("Closing Websocket")
		c.Close()
	}()
	clients[c] = struct{}{}

	for {
		var msg Message
		err := c.ReadJSON(&msg)
		if err != nil {
			log.Printf("Error in reading json message. Error : %v", err)
			return
		}

		// process the message
		broadcast(msg)
	}
}
```
Let’s see what we are doing here.
- We’ve created a map `clients` to store the websocket connections
- Inside handleClient, we make sure to store the client connection in the clients map
- Then we spin up an indefinite for loop to listen for messages from the connection.
- Whenever we receive a message from the connection, it’s read using the ReadJSON method that reads and marshals the data into a Message struct.

Now that we’ve read the message, we’ll see more on how we can broadcast it to all other websocket connections

```go
func broadcast(msg Message) {
	for conn := range clients {
		conn.WriteJSON(msg)
	}
}
```

The above function simply takes in a message and writes it to all websocket connections using the `WriteJSON` method using a for loop.

Now, let’s write a simple index.html file and save it inside a static folder that will act as the chat interface.

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

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go chat 🚀</title>
</head>

<body>
    <div style="display: flex;flex-direction: column;">
        <div id="inputs">
            <form onsubmit="handleSubmit(event)" method="post">
                <input id="username" type="text" name="username" placeholder="username" required>
                <input id="message" type="text" name="message" placeholder="what's on your mind ?" required>
                <input type="submit" value="Send">
            </form>
        </div>
        <div id="messages" style="display: flex;flex-direction: column;"></div>
    </div>
</body>

<script>

    const websocket = new WebSocket("ws://localhost:8080/ws");

    function handleSubmit(e) {
        e.preventDefault();
        websocket.send(JSON.stringify({
            "from": e.target.username.value,
            "message": e.target.message.value
        }))
    }

    websocket.onmessage = function (event) {
        const message = JSON.parse(event.data)
        messages.innerHTML += `<p><b>${message.from}</b> says ${message.message}</p>`
    }
</script>

</html>
```

The above HTML file, when opened, creates a websocket connection with the backend and sends a message whenever the form is submitted. It also listens for incoming messages using the onmessage handler.

Let’s update the main function to serve the index.html and also wire up the websocket handler.

```go
...

func main() {
	...
	router.StaticFile("/", "./static/index.html")
	router.GET("/ws", serveWs)
...
}
...

```

The final main.go file looks something like this.
```go

package main

import (
	"log"
	"net/http"
	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
)

func main() {

	router := gin.Default()
	router.StaticFile("/", "./static/index.html")
	router.GET("/ws", serveWs)
	err := router.Run()
	if err != nil {
		log.Fatalf("Unable to start server. Error %v", err)
	}
	log.Println("Server started successfully.")
}

func serveWs(c *gin.Context) {

	upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		log.Printf("Error in upgrading web socket. Error: %v", err)
		return
	}

	go handleClient(conn)
}

var clients = make(map[*websocket.Conn]struct{})

type Message struct {
	From    string `json:"from"`
	Message string `json:"message"`
}

func broadcast(msg Message) {
	for conn := range clients {
		conn.WriteJSON(msg)
	}
}

func handleClient(c *websocket.Conn) {
	defer func() {
		delete(clients, c)
		log.Println("Closing Websocket")
		c.Close()
	}()
	clients[c] = struct{}{}

	for {
		var msg Message
		err := c.ReadJSON(&msg)
		if err != nil {
			log.Printf("Error in reading json message. Error : %v", err)
			return
		}

		broadcast(msg)
	}
}


```

Now let’s run the application and open `localhost:8080` in a browser.

```bash
go run .
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0macmlgdo4f3p02ug10y.gif)

Ta-da ✨! We’ve successfully built a simple chat application with Websockets in less than 100 lines of code.

However, this solution has one big drawback i.e, it cannot be deployed in a scalable manner.

In the next part, we’ll go through in detail about this problem and how we can solve it with Redis pub sub. Stay tuned !