How to Build a Concurrent Chat App with Go and Websockets
Tags: webdev, tutorial
Published: 2020-11-30 15:34:24

[Go](https://golang.org/) emerged from Google out of a need to build highly performant applications using an easy-to-understand syntax. It's a statically typed, compiled language developed by some of the innovators of C, without the programming burden of manual memory management. Primarily, it was designed to take advantage of modern multicore CPUs and networked machines.

In this post, I'll demonstrate the capabilities of Go. We'll take advantage of Go's ability to easily create concurrent apps to build a chat app. On the backend, we'll use Redis as the intermediary to accept messages from the browser and send them to the subscribed clients. On the frontend, we'll use websockets via[ socket.io](https://socket.io/) to facilitate the client-side communication. We'll deploy it all on[ Heroku](https://www.heroku.com), a PaaS provider that makes it easy to deploy and host your apps. Just as Go makes programming such an application simple, Heroku makes it easy to supplement it with additional infrastructure.


## Channels in Go

What developers find appealing about Go is its ability to communicate concurrently, which it does through a system called _channels_. It's important to draw upon[ an oft-cited distinction between concurrency and parallelism](https://blog.golang.org/waza-talk). Parallelism is the process by which a CPU executes multiple tasks at the same time, while concurrency is the CPU's ability to switch between multiple tasks, which start, run,  and complete while overlapping one another. In other words, parallel programs handle many operations at once, while concurrent programs can switch between many operations over the same period of time.

A channel in Go is the conduit through which concurrency flows. Channels can be unidirectional—with data either sent to or received by them—or bidirectional, which can do both. Here's an example that demonstrates the basic principles of concurrency and channels:


```
func one(c1 chan string) {
  for i := 0; i < 5; i++ {
    c1 <- "Channel One"
  }
  close(c1)
}

func two(c2 chan string) {
  for i := 0; i < 5; i++ {
    c2 <- "Channel Two"
  }
  close(c2)
}

func main() {
  c1 := make(chan string)
  c2 := make(chan string)

  go one(c1)
  go two(c2)

  for {
    select {
      case msg, ok := <-c1:
      fmt.Println(msg)
      if !ok {
        c1 = nil
      }
      case msg, ok := <-c2:
      fmt.Println(msg)
      if !ok {
        c2 = nil
      }
    }
    if c1 == nil && c2 == nil {
       break
    }
  }
}
```


[You can run this example online at the Go Playground to see the results](https://play.golang.org/p/_FKRgsC7ptB). Channels are created by first specifying the data type which they will communicated with—in this case, `string`. Two[ goroutines](https://tour.golang.org/concurrency/1), `one `and `two`, accept each of these channels as an argument. Both then loop five times, passing a message to the channel, which is indicated by the `&lt;-` glyph. Meanwhile, in the `main `function, an infinite for loop, waits for messages to come in from the channels. The `select `statement picks the channel which has a pending message, prints it, and moves on. If the channel was closed (which is important not just for memory management, but to also indicate that no more data will be sent), the channel is set to nil. When both channels are nil, the loop breaks.

In essence, a receiver is waiting endlessly to receive packets of data. When it receives the data, it acts upon it, then continues to wait for more messages. These receivers operate concurrently, without interrupting the rest of the program's flow. For this chat application, we will wait for a user to send a message to a receiver over a channel. When the message is received, the app will broadcast it to the frontend, so that everyone sitting in chat can read the text.


## Prerequisites

You should have a relatively recent version of Golang installed; anything past 1.12 will do. Create a directory in your[ GOPATH](https://golang.org/doc/gopath_code.html) called `heroku_chat_sample`. If you'd like to run the code locally, you can also install and run a[ Redis](https://redis.io/) server—but this is definitely not required, as a Heroku add-on will provide this for us in production.


## Building a Simple Server

Let's start with a quick and easy "Hello World" server to verify that we can run Go programs. We'll start by fetching[ Gorilla](https://www.gorillatoolkit.org/), a web toolkit that simplifies the process of writing HTTP servers:


```
go get -u github.com/gorilla/mux
```


Next, create a file called `main.go`, and paste these lines into it:


```
package main
import (
  "fmt"
  "log"
  "net/http"
  "github.com/gorilla/mux"
)

func main() {
  r := mux.NewRouter()
  r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, world!")
  })
  log.Print("Server starting at localhost:4444")
  http.ListenAndServe(":4444", r)
}
```


Finally, enter `go run main.go` in your terminal. You should be able to visit `localhost:4444` in the browser, and see the greeting. With just these few lines, we can get a better sense of how to create routes using Gorilla.

But static text is boring, right? Let's have this server show an HTML file. Create a directory called `public`, and within that, create a file called `index.html` that looks like this:


```
<!DOCTYPE html>
<html>
<head>
   <title>Go Chat!</title>
   <link
     rel="stylesheet"
     href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
   />
   <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
   <div class="container">
     <div class="jumbotron">
       <h1>Go Chat!</h1>
     </div>
     <form id="input-form" class="form-inline">
       <div class="form-group">
         <input
           id="input-username"
           type="text"
           class="form-control"
          placeholder="Enter username"
         />
       </div>
       <div class="form-group">
         <input
           id="input-text"
           type="text"
           class="form-control"
          placeholder="Enter chat text here"
         />
       </div>
       <button class="btn btn-primary" type="submit">Send</button>
     </form>
     <div id="chat-text"></div>
   </div>
</body>
<script type="text/javascript" src="app.js"></script>
</html>
```


There's some JavaScript necessary for this page to communicate with the server; let's create a placeholder app.js file now:


```
window.addEventListener('DOMContentLoaded', (_) => {
  form.addEventListener("submit", function (event) {
     event.preventDefault();
     let username = document.getElementById("input-username");
     let text = document.getElementById("input-text");
     text.value = "";
  });
});
```


Then, let's change our server code to look like this:


```
package main
import (
  "log"
  "net/http"
 )
func main() {
  http.Handle("/", http.FileServer(http.Dir("./public")))
  log.Print("Server starting at localhost:4444")
  if err := http.ListenAndServe(":4444", nil); err != nil {
    log.Fatal(err)
  }
}
```


If you restart the server and head back to `localhost:4444`, you should see a page inviting you to chat. It won't do much yet, but it's a start!

![golang1](https://dev-to-uploads.s3.amazonaws.com/i/5v01mtqkc1xrm8i9iaq1.png)
 

Let's make one more minor change to see this app on the way to becoming[ a twelve-factor app](https://12factor.net/config): store our port number in an environment variable. This won't be hugely important right now in development, but it will make a difference when we deploy the app to production.

Create a file called `.env` and paste this line into it:


```
PORT=4444
```


Then, fetch the[ godotenv](https://github.com/joho/godotenv) modules:


```
go get github.com/joho/godotenv
```


And lastly, let's change the server code one more time to accept this environment variable:


```
err := godotenv.Load()
if err != nil {
  log.Fatal("Error loading .env file")
}

port := os.Getenv("PORT")
// Same code as before
if err := http.ListenAndServe(":"+port, nil); err != nil {
  log.Fatal(err)
}
```


In short, so long as `GO_ENV` is empty, we will load our environment variables from whatever is defined locally in `.env`. Otherwise, the app expects the environment variables to be set by the system, which we will do when the time comes.


## Establishing Communication Using websockets and Redis

[Websockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) are a useful technique to pass messages from the client/browser to the server. It will be the fundamental technology used to send and receive chat messages from all the users in our chat room. On the backend, we will use Redis to store the chat history, so that any new user can instantly get all of the room's previous messages. Redis is an in-memory database, which is often used for caching. For this project, we don't need the heft of a relational database, but we do want some kind of storage system to keep track of users and their messages.


### Setting up Redis

To start with, let's prepare to introduce Redis as a dependency. If you have Redis running locally, you'll need to add a new line to specify the host and port of your Redis instance in your `.env` file:


```
REDIS_URL=127.0.0.1:6379
```


Grab the Redis module as a dependency from GitHub:


```
go get -u github.com/gomodule/redigo/redis
```


We'll set up our Redis client as a global variable to make life easier:


```
var (
  rdb *redis.Client
)
```


Then, in our `main()` function, we will create an instance of this client via the environment variable:


```
redisURL := os.Getenv("REDIS_URL")
opt, err := redis.ParseURL(redisURL)
if err != nil {
  panic(err)
}
rdb = redis.NewClient(opt)
```


We're using environment variables here because the server address is likely to be different than the one we use in development, but we don't want to hardcode those values. If you don't have a Redis server running locally, don't worry—you can still follow along in the tutorial, and see the result in your browser live after we publish the app to Heroku.

When the server starts up, it'll connect to Redis first before listening for any incoming connections.


### Setting up websockets

Configuring our websockets is a little bit trickier, particularly because we need to jump into some JavaScript code to finish wiring that up. However, before we get there, let's take a step back and remember what we're trying to do. A user will visit a webpage, assign themselves a username, and send messages in a chat room. It's fair to say that the smallest chunk of data would be the user's name and their message. Let's set up a data structure in Go that captures this:


```
type ChatMessage struct {
  Username string`json:"username"`
  Text     string`json:"text"`
}
```


Since we're going to be communicating with the frontend, it's useful to prepare to think about this structure in terms of how it will be represented in JSON.

Next, let's add two more lines of functionality to our web server in `main()`. The first line will indicate which function we want to run whenever a new websocket connection is opened—in other words, whenever a new user joins. The second line will set up a long-running goroutine which decides what to do whenever a user sends a message:


```
http.HandleFunc("/websocket", handleConnections)
go handleMessages()
```


Last, let's jump back to the top of the file and add some global variables. We'll explain what they're for after the code:


```
var clients = make(map[*websocket.Conn]bool)
var broadcaster = make(chan ChatMessage)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
  returntrue
},
}
```


In these lines:



*   `clients` is a list of all the currently active clients (or open websockets)
*   `broadcaster `is a single channel which is responsible for sending and receiving our `ChatMessage `data structure
*   `upgrader `is a bit of a clunker; it's necessary to "upgrade" Gorilla's incoming requests into a websocket connection


### Sending a Message

Let's start building out `handleConnections `first. When a new user joins the chat, three things should happen:



1. They should be set up to receive messages from other clients.
2. They should be able to send their own messages.
3. They should receive a full history of the previous chat (backed by Redis).

Addressing number one is simple with Gorilla. We'll create a new client, and append it to our global `clients `list in just a few lines:


```
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
  log.Fatal(err)
}
// ensure connection close when function returns
defer ws.Close()
clients[ws] = true
```


Let's look at sending messages next instead:


```
for {
var msg ChatMessage
// Read in a new message as JSON and map it to a Message object
err := ws.ReadJSON(&msg)
if err != nil {
  delete(clients, ws)
  break
}
// send new message to the channel
broadcaster <- msg
}
```


After a client websocket is opened and added to the `clients `pool, an infinite for loop will run endlessly. Unlike other languages, infinite loops are practically encouraged in Go. The trick is to remember to `break` out of them, and to clean up after yourself when you do. Here, the websocket is just endlessly looking for messages that the client has sent: `ws.ReadJSON(&msg)` is checking to see if `msg `is populated. If `msg `is ever not `nil`, it'll send the message over to the `broadcaster `channel. That's pretty much it as far as sending messages goes. If this websocket has an issue afterwards, it'll remove itself from the clients pool--`delete(clients, ws)`, and then break out of this loop, severing its connection.

What happens when a `msg `is sent to the `broadcaster `channel? That's where `handleMessages `comes in.


### Receiving Messages

It's the responsibility of `handleMessages `to send any new messages to every connected client. Just like the sending of messages, it all starts with an infinite `for` loop:


```
func handleMessages() {
  for {
    // grab any next message from channel
    msg := <-broadcaster
  }
}
```


This line does nothing until something is sent to the channel. This is the core of goroutines, concurrency, and channels. Concurrency depends on channels to communicate with one another. If there's no data being sent, there's nothing to reason about or work around. When a `msg `is received, we can send it to all the open clients:


```
for client := range clients {
   err := client.WriteJSON(msg)
   if err != nil && unsafeError(err) {
     log.Printf("error: %v", err)
     client.Close()
     delete(clients, client)
   }
}
```


We iterate over every client using the `range `operator; for each client, instead of reading JSON, we're writing it back out. Again, what comes after this is handled on the JavaScript side of things. If there's an issue with this write, we'll print a message, close the client, and remove it from the global list.


### Saving and Restoring History

But what about our final feature, which requires that every new client has access to the full chat history? We'll need to use Redis for that, and in particular, two operations:



1. Any new message should be added to a list of running messages.
2. Any new user should receive that full list.

When sending new messages, we can store them as a list in Redis using[ RPUSH](https://redis.io/commands/rpush):


```
rdb.RPush("chat_messages", json)
```


When a new user joins, we can send the entire list at once using[ LRANGE](https://redis.io/commands/lrange):


```
chatMessages, err := rdb.LRange("chat_messages", 0, -1).Result()
if err != nil {
  panic(err)
}
```


This application is a bit tricky, because we need to send all the messages to just a single client. However, we can assume that only new connections call `handleConnections`, and at any point before the infinite for loop, we can communicate to this client and send them our messages. Our code would look something like this:


```
// send previous messages
for _, chatMessage := range chatMessages {
var msg ChatMessage
json.Unmarshal([]byte(chatMessage), &msg)
   err := client.WriteJSON(msg)
   if err != nil && unsafeError(err) {
     log.Printf("error: %v", err)
     client.Close()
     delete(clients, client)
   }
}
```



## Full Backend Code

He's what our complete Go code would look like: https://gist.github.com/gjtorikian/8894dec140a6e57934572f5b447f6d51

 


## The Frontend

Since this post focuses on Go and Heroku, we won't go into many details about the JavaScript code. However, it's only about 25 lines, so there's not much to go into! 

Our previous `index.html` can stay the same. Let's replace the contents of app.js with the following:


```
$(function () {
  let websocket = newWebSocket("wss://" + window.location.host + "/websocket");
  let room = $("#chat-text");
  websocket.addEventListener("message", function (e) {
    let data = JSON.parse(e.data);
    let chatContent = `<p><strong>${data.username}</strong>: ${data.text}</p>`;
    room.append(chatContent);
    room.scrollTop = room.scrollHeight; // Auto scroll to the bottom
  });
```


  `$("#input-form").on("submit", function (event) {`


```
   event.preventDefault();
   let username = $("#input-username")[0].value;
   let text = $("#input-text")[0].value;
   websocket.send(
     JSON.stringify({
       username: username,
       text: text,
     })
   );
   $("#input-text")[0].value = "";
  });
});
```


Let's break this down into chunks. The first lines (`let websocket` and `let room`) just set up some global variables we can use later on.

`websocket.addEventListener` is responsible for handling any new messages the client receives. In other words, it's the frontend code corresponding to `handleMessages`. When `handleMessages `writes JSON, it sends it as an event called `message`. From there, the JavaScript can parse the data out, style it a bit, and append the text to the chat room.

Similarly, the form logic sends data using websockets to our previous `ws.ReadJSON` line. Any time the form is submitted, the JavaScript takes note of who said something and what they said. It then sends the message to the websocket so that the Go code can store it in Redis, and notify all the clients.


## Deploying to Heroku

You're now ready to deploy this app to Heroku! If you don't have one already, be sure to create a[ free account on Heroku](https://signup.heroku.com/), then install[ the Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli), which makes creating apps and attaching add-ons much easier.

First, log into your account:


```
heroku login
```


Next, let's create a new app using create:


```
heroku create
```


You'll be assigned a random name; I've got `evening-wave-98825`, so I'll be referring to that here.

Next, create a[ Procfile](https://devcenter.heroku.com/articles/procfile). A Procfile specifies which commands to run when your app boots up, as well as setting up any workers. 

Ours will be a single line:


```
web: bin/heroku_chat_sample
```


Since we need Redis, we can attach the free instance for our demo app:


```
heroku addons:create heroku-redis:hobby-dev -a evening-wave-98825
```


Let's build the app, and commit everything we have:


```
go mod init
go mod vendor
go build -o bin/heroku_chat_sample -v .
git init
git add .
git commit -m "First commit of chat app"
```


And let's send it all to Heroku:


```
heroku git:remote -a evening-wave-98825
git push heroku main
```


This process is all you need to deploy everything into production. If you visit the URL Heroku generated for you, you should see your chat app. It may look basic, but there's a lot going on behind the scenes!

You can download all of the code used in this article[ here](https://github.com/gjtorikian/heroku_chat_sample).


## More Information

If you enjoyed how easy it was to deploy a Go app with Redis onto Heroku, this is just the beginning! Here's[ another tutorial](https://devcenter.heroku.com/articles/getting-started-with-go) on building something exciting with Go. If you'd like to know more about how Go works with Heroku, here's[ another article with all the details](https://devcenter.heroku.com/articles/go-support).