DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • How to Build a Pokedex React App with a Slash GraphQL Backend
  • Non-blocking Database Migrations
  • Build a Java Microservice With AuraDB Free
  • How to Build a Full-Stack App With Next.js, Prisma, Postgres, and Fastify

Trending

  • What’s Got Me Interested in OpenTelemetry—And Pursuing Certification
  • Developers Beware: Slopsquatting and Vibe Coding Can Increase Risk of AI-Powered Attacks
  • Proactive Security in Distributed Systems: A Developer’s Approach
  • How To Build Resilient Microservices Using Circuit Breakers and Retries: A Developer’s Guide To Surviving
  1. DZone
  2. Data Engineering
  3. Data
  4. How to Build a Concurrent Chat App With Go and WebSockets

How to Build a Concurrent Chat App With Go and WebSockets

Just as Go makes programming such an application simple, Heroku makes it easy to supplement it with additional infrastructure.

By 
Michael Bogan user avatar
Michael Bogan
DZone Core CORE ·
Dec. 01, 20 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
23.1K Views

Join the DZone community and get the full member experience.

Join For Free

Go 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 C's innovators, 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 create concurrent apps to build a chat app easily. 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 to facilitate client-side communication. We'll deploy it all on Heroku, 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. Parallelism is the process by which a CPU executes multiple tasks simultaneously. Simultaneously, 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:

Go
 




xxxxxxxxxx
1
39


 
1
func one(c1 chan string) {
2
  for i := 0; i < 5; i++ {
3
    c1 <- "Channel One"
4
  }
5
  close(c1)
6
}
7

          
8
func two(c2 chan string) {
9
  for i := 0; i < 5; i++ {
10
    c2 <- "Channel Two"
11
  }
12
  close(c2)
13
}
14

          
15
func main() {
16
  c1 := make(chan string)
17
  c2 := make(chan string)
18

          
19
  go one(c1)
20
  go two(c2)
21

          
22
  for {
23
    select {
24
      case msg, ok := <-c1:
25
      fmt.Println(msg)
26
      if !ok {
27
        c1 = nil
28
      }
29
      case msg, ok := <-c2:
30
      fmt.Println(msg)
31
      if !ok {
32
        c2 = nil
33
      }
34
    }
35
    if c1 == nil && c2 == nil {
36
      break
37
    }
38
  }
39
}



You can run this example online at the Go Playground to see the results. Channels are created by first specifying the data type they will communicate with — in this case, string. Two goroutines, one and two, accept each of these channels as an argument. Both then loop five times, passing a message to the channel, indicated by the <- glyph. Meanwhile, in the main function, an infinite 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 also indicates 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 front end. 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 called heroku_chat_sample. If you'd like to run the code locally, you can also install and run a Redis 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, 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:

Go
 




xxxxxxxxxx
1
16


 
1
package main
2
  import (
3
    "fmt"
4
    "log"
5
    "net/http"
6
    "github.com/gorilla/mux"
7
  )
8

          
9
  func main() {
10
    r := mux.NewRouter()
11
    r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
12
    fmt.Fprintf(w, "Hello, world!")
13
  })
14
  log.Print("Server starting at localhost:4444")
15
  http.ListenAndServe(":4444", r)
16
}



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

But the 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:

HTML
 




xxxxxxxxxx
1
39


 
1
<!DOCTYPE html>
2
<html>
3
<head>
4
  <title>Go Chat!</title>
5
  <link
6
    rel="stylesheet"
7
    href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
8
  />
9
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
10
</head>
11
<body>
12
  <div class="container">
13
  <div class="jumbotron">
14
    <h1>Go Chat!</h1>
15
  </div>
16
  <form id="input-form" class="form-inline">
17
    <div class="form-group">
18
      <input
19
        id="input-username"
20
        type="text"
21
        class="form-control"
22
        placeholder="Enter username"
23
      />
24
    </div>
25
    <div class="form-group">
26
      <input
27
        id="input-text"
28
        type="text"
29
        class="form-control"
30
        placeholder="Enter chat text here"
31
      />
32
    </div>
33
    <button class="btn btn-primary" type="submit">Send</button>
34
  </form>
35
  <div id="chat-text"></div>
36
  </div>
37
</body>
38

          
39
</html>



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

JavaScript
 




xxxxxxxxxx
1


 
1
window.addEventListener('DOMContentLoaded', (_) => {
2
  form.addEventListener("submit", function (event) {
3
    event.preventDefault();
4
    let username = document.getElementById("input-username");
5
    let text = document.getElementById("input-text");
6
    text.value = "";
7
  });
8
});



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

Go
 




xxxxxxxxxx
1
14


 
1
package main
2

          
3
import (
4
  "log"
5
  "net/http"
6
 )
7

          
8
func main() {
9
  http.Handle("/", http.FileServer(http.Dir("./public")))
10
  log.Print("Server starting at localhost:4444")
11
  if err := http.ListenAndServe(":4444", nil); err != nil {
12
    log.Fatal(err)
13
  }
14
}



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!

go chat
Sample of the chat application

Let's make one more minor change to see this app on the way to becoming a twelve-factor app: 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 deploying the app to production.

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

PORT=4444

Then, fetch the godotenv modules:

get github.com/joho/godotenv

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

Go
 




x


 
1
err := godotenv.Load()
2
  if err != nil {
3
  log.Fatal("Error loading .env file")
4
}
5

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



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 system's environment variables, which we will do when the time comes.

Establishing Communication Using WebSockets and Redis

Websockets 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. We don't need the heft of a relational database for this project, 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:

Go
x
 
1
var (
2
  rdb *redis.Client
3
)


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

Go
 




xxxxxxxxxx
1


 
1
redisURL := os.Getenv("REDIS_URL")
2
opt, err := redis.ParseURL(redisURL)
3
if err != nil {
4
  panic(err)
5
}
6
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 message. Let's set up a data structure in Go that captures this:

Go
 




xxxxxxxxxx
1


 
1
type ChatMessage struct {
2
  Username string`json:"username"`
3
  Text string`json:"text"`
4
}



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:

Go
 




xxxxxxxxxx
1


 
1
http.HandleFunc("/websocket", handleConnections)
2
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:

Go
 




xxxxxxxxxx
1


 
1
var clients = make(map[*websocket.Conn]bool)
2
var broadcaster = make(chan ChatMessage)
3
var upgrader = websocket.Upgrader{
4
CheckOrigin: func(r *http.Request) bool {
5
  returntrue
6
},
7
}



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 client's list in just a few lines:

Go
 




xxxxxxxxxx
1


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



Let's look at sending messages next instead:

Go
 




xxxxxxxxxx
1
11


 
1
for {
2
var msg ChatMessage
3
// Read in a new message as JSON and map it to a Message object
4
err := ws.ReadJSON(&msg)
5
if err != nil {
6
  delete(clients, ws)
7
  break
8
}
9
// send new message to the channel
10
broadcaster <- msg
11
}



After a client WebSocket is opened and added to the client's pool, an infinite 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 afterward, it'll remove itself from the client 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:

Go
 




xxxxxxxxxx
1


 
1
func handleMessages() {
2
  for {
3
    // grab any next message from channel
4
    msg := <-broadcaster
5
  }
6
}



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 workaround. When a msg is received, we can send it to all the open clients:

Go
 




xxxxxxxxxx
1


 
1
for client := range clients {
2
err := client.WriteJSON(msg)
3
if err != nil && unsafeError(err) {
4
log.Printf("error: %v", err)
5
client.Close()
6
delete(clients, client)
7
}
8
}



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:

rdb.RPush("chat_messages", json)

When a new user joins, we can send the entire list at once using LRANGE:

Go
 




xxxxxxxxxx
1


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



This application is a bit tricky because we need to send all the messages to 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:

Go
 




xxxxxxxxxx
1
11


 
1
// send previous messages
2
for _, chatMessage := range chatMessages {
3
var msg ChatMessage
4
json.Unmarshal([]byte(chatMessage), &msg)
5
  err := client.WriteJSON(msg)
6
  if err != nil && unsafeError(err) {
7
    log.Printf("error: %v", err)
8
    client.Close()
9
    delete(clients, client)
10
  }
11
}



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:

JavaScript
 




xxxxxxxxxx
1
22


 
1
$(function () {
2
  let websocket = newWebSocket("wss://" + window.location.host + "/websocket");
3
  let room = $("#chat-text");
4
  websocket.addEventListener("message", function (e) {
5
    let data = JSON.parse(e.data);
6
    let chatContent = `<p><strong>${data.username}</strong>: ${data.text}</p>`;
7
    room.append(chatContent);
8
    room.scrollTop = room.scrollHeight; // Auto scroll to the bottom
9
  });
10
$("#input-form").on("submit", function (event) {
11
  event.preventDefault();
12
  let username = $("#input-username")[0].value;
13
  let text = $("#input-text")[0].value;
14
  websocket.send(
15
    JSON.stringify({
16
      username: username,
17
      text: text,
18
    })
19
  );
20
  $("#input-text")[0].value = "";
21
  });
22
});



Let's break this down into chunks. The first lines (let websocket and let room) 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. Whenever 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, then install the 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. A Procfile specifies which commands to run when your app boots up and set 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:

Plain Text
 




xxxxxxxxxx
1


 
1
go mod init
2
go mod vendor
3
go build -o bin/heroku_chat_sample -v .
4
git init
5
git add .
6
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.

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 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.

app WebSocket Redis (company) Build (game engine) Database Data (computing) IT Data Types JavaScript

Opinions expressed by DZone contributors are their own.

Related

  • How to Build a Pokedex React App with a Slash GraphQL Backend
  • Non-blocking Database Migrations
  • Build a Java Microservice With AuraDB Free
  • How to Build a Full-Stack App With Next.js, Prisma, Postgres, and Fastify

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!