Skip Navigation
Show nav
Dev Center
  • Get Started
  • Documentation
  • Changelog
  • Search
  • Get Started
    • Node.js
    • Ruby on Rails
    • Ruby
    • Python
    • Java
    • PHP
    • Go
    • Scala
    • Clojure
    • .NET
  • Documentation
  • Changelog
  • More
    Additional Resources
    • Home
    • Elements
    • Products
    • Pricing
    • Careers
    • Help
    • Status
    • Events
    • Podcasts
    • Compliance Center
    Heroku Blog

    Heroku Blog

    Find out what's new with Heroku on our blog.

    Visit Blog
  • Log inorSign up
Hide categories

Categories

  • Heroku Architecture
    • Compute (Dynos)
      • Dyno Management
      • Dyno Concepts
      • Dyno Behavior
      • Dyno Reference
      • Dyno Troubleshooting
    • Stacks (operating system images)
    • Networking & DNS
    • Platform Policies
    • Platform Principles
  • Developer Tools
    • Command Line
    • Heroku VS Code Extension
  • Deployment
    • Deploying with Git
    • Deploying with Docker
    • Deployment Integrations
  • Continuous Delivery & Integration (Heroku Flow)
    • Continuous Integration
  • Language Support
    • Node.js
      • Working with Node.js
      • Node.js Behavior in Heroku
      • Troubleshooting Node.js Apps
    • Ruby
      • Rails Support
      • Working with Bundler
      • Working with Ruby
      • Ruby Behavior in Heroku
      • Troubleshooting Ruby Apps
    • Python
      • Working with Python
      • Background Jobs in Python
      • Python Behavior in Heroku
      • Working with Django
    • Java
      • Java Behavior in Heroku
      • Working with Java
      • Working with Maven
      • Working with Spring Boot
      • Troubleshooting Java Apps
    • PHP
      • PHP Behavior in Heroku
      • Working with PHP
    • Go
      • Go Dependency Management
    • Scala
    • Clojure
    • .NET
      • Working with .NET
  • Databases & Data Management
    • Heroku Postgres
      • Postgres Basics
      • Postgres Getting Started
      • Postgres Performance
      • Postgres Data Transfer & Preservation
      • Postgres Availability
      • Postgres Special Topics
      • Migrating to Heroku Postgres
    • Heroku Key-Value Store
    • Apache Kafka on Heroku
    • Other Data Stores
  • AI
    • Model Context Protocol
    • Vector Database
    • Heroku Inference
      • Inference Essentials
      • AI Models
      • Inference API
      • Quick Start Guides
    • Working with AI
  • Monitoring & Metrics
    • Logging
  • App Performance
  • Add-ons
    • All Add-ons
  • Collaboration
  • Security
    • App Security
    • Identities & Authentication
      • Single Sign-on (SSO)
    • Private Spaces
      • Infrastructure Networking
    • Compliance
  • Heroku Enterprise
    • Enterprise Accounts
    • Enterprise Teams
    • Heroku Connect (Salesforce sync)
      • Heroku Connect Administration
      • Heroku Connect Reference
      • Heroku Connect Troubleshooting
  • Patterns & Best Practices
  • Extending Heroku
    • Platform API
    • App Webhooks
    • Heroku Labs
    • Building Add-ons
      • Add-on Development Tasks
      • Add-on APIs
      • Add-on Guidelines & Requirements
    • Building CLI Plugins
    • Developing Buildpacks
    • Dev Center
  • Accounts & Billing
  • Troubleshooting & Support
  • Integrating with Salesforce
  • Language Support
  • Go
  • Using WebSockets on Heroku with Go

Using WebSockets on Heroku with Go

English — 日本語に切り替える

Last updated July 12, 2024

Table of Contents

  • Prerequisites
  • Create WebSocket app
  • Functionality
  • Back-end
  • Front-end
  • Run locally
  • Next steps

This tutorial will get you going with a Go application that uses a WebSocket, deployed to Heroku.

Sample code for the demo application is available on GitHub. Edits and enhancements are welcome. Just fork the repository, make your changes and send us a pull request.

Prerequisites

  • Go, Git, Godep and the Heroku client (as described in Getting Started with Go).
  • A Heroku user account. Signup is free and instant.

Create WebSocket app

The sample application provides a simple example of using WebSockets with Go. Get the sample app and follow along with the code as you read.

Get the sample app

$ go get github.com/heroku-examples/go-websocket-chat-demo/...
$ cd $GOPATH/src/github.com/heroku-examples/go-websocket-chat-demo

Functionality

The sample application is a simple chat application that will open a WebSocket to the back-end. Any time a chat message is sent from the browser, it’s sent to the server and then published to a Redis channel. A separate goroutine is subscribed to the same Redis channel and broadcasts the received message to all open WebSocket connections.

There are a few key pieces to this example:

  1. The Gorilla WebSocket library which provides a complete WebSocket implementation.
  2. The Logrus library which provides structured, pluggable logging.
  3. The redigo library which provides an interface to Redis.
  4. JavaScript on the browser that opens a WebSocket connection to the server and responds to a WebSocket message received from the server.

Let’s take a look at both the back-end and front-end pieces in more detail.

Back-end

With Gorilla’s WebSocket library, we can create WebSocket handlers, much like standard http.Handlers. In this case, we’ll have one endpoint that handles sending and receiving messages named handleWebsocket. A websocket.Upgrader needs to be used to upgrade incoming requests to WebSocket connections. The endpoint is used for both submitting new messages to and receiving messages from the chat service. Incoming messages are received via ws.ReadMessage() as Go byte slices. We take those messages, validate them and insert them into our Redis subscription channel, so all connected servers can receive updates.

// handleWebsocket connection.
func handleWebsocket(w http.ResponseWriter, r *http.Request) {
    if r.Method != "GET" {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        m := "Unable to upgrade to websockets"
        log.WithField("err", err).Println(m)
        http.Error(w, m, http.StatusBadRequest)
        return
    }

    id := rr.register(ws)

    for {
        mt, data, err := ws.ReadMessage()
        l := log.WithFields(logrus.Fields{"mt": mt, "data": data, "err": err})
        if err != nil {
            if err == io.EOF {
                l.Info("Websocket closed!")
            } else {
                l.Error("Error reading websocket message")
            }
            break
        }
        switch mt {
        case websocket.TextMessage:
            msg, err := validateMessage(data)
            if err != nil {
                l.WithFields(logrus.Fields{"msg": msg, "err": err}).Error("Invalid Message")
                break
            }
            rw.publish(data)
        default:
            l.Warning("Unknown Message!")
        }
    }

    rr.deRegister(id)

    ws.WriteMessage(websocket.CloseMessage, []byte{})
}

Messages received from Redis are broadcast to all WebSocket connections by the redisReceivers run() func.

func (rr *redisReceiver) run() error {
    l := log.WithField("channel", Channel)
    conn := rr.pool.Get()
    defer conn.Close()
    psc := redis.PubSubConn{Conn: conn}
    psc.Subscribe(Channel)
    for {
        switch v := psc.Receive().(type) {
        case redis.Message:
            l.WithField("message", string(v.Data)).Info("Redis Message Received")
            if _, err := validateMessage(v.Data); err != nil {
                l.WithField("err", err).Error("Error unmarshalling message from Redis")
                continue
            }
            rr.broadcast(v.Data)
        case redis.Subscription:
            l.WithFields(logrus.Fields{
                "kind":  v.Kind,
                "count": v.Count,
            }).Println("Redis Subscription Received")
        case error:
            return errors.Wrap(v, "Error while subscribed to Redis channel")
        default:
            l.WithField("v", v).Info("Unknown Redis receive during subscription")
        }
    }
}

Because of this architectural model, you can run this application on as many dynos as you want and all of your users will be able to send and receive updates.

In the main() func we create a redisReceiver and redisWritter and run() them to handle our Redis interactions. We register the handleWebsocket handler at /ws. The public directory will be served by http.FileServer out of public/. We take special care to handle availability of the redis server, which may not be available on boot and may be restarted as part of a maintenance.

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        log.WithField("PORT", port).Fatal("$PORT must be set")
    }

    redisURL := os.Getenv("REDIS_URL")
    redisPool, err := redis.NewRedisPoolFromURL(redisURL)
    if err != nil {
        log.WithField("url", redisURL).Fatal("Unable to create Redis pool")
    }

    rr = newRedisReceiver(redisPool)
    rw = newRedisWriter(redisPool)

    go func() {
        for {
            waited, err := redis.WaitForAvailability(redisURL, waitTimeout, rr.wait)
            if !waited || err != nil {
                log.WithFields(logrus.Fields{"waitTimeout": waitTimeout, "err": err}).Fatal("Redis not available by timeout!")
            }
            rr.broadcast(availableMessage)
            err = rr.run()
            if err == nil {
                break
            }
            log.Error(err)
        }
    }()

    go func() {
        for {
            waited, err := redis.WaitForAvailability(redisURL, waitTimeout, nil)
            if !waited || err != nil {
                log.WithFields(logrus.Fields{"waitTimeout": waitTimeout, "err": err}).Fatal("Redis not available by timeout!")
            }
            err = rw.run()
            if err == nil {
                break
            }
            log.Error(err)
        }
    }()

    http.Handle("/", http.FileServer(http.Dir("./public")))
    http.HandleFunc("/ws", handleWebsocket)
    log.Println(http.ListenAndServe(":"+port, nil))
}

Front-end

The second part of this is setting up the client side to open the WebSockets connection with the server.

The index page uses Bootstrap for CSS and jQuery. We store all of our static assets in the public folder.

The main WebSocket interaction happens in public/js/application.js, which is loaded by the main page. It opens our WebSocket connection to the server. We use reconnecting-websocket, which automatically reconnects any broken connection in the browser.

With an open WebSocket, the browser will receive messages and we define a function to handle this.

box.onmessage = function(message) {
  var data = JSON.parse(message.data);
  $("#chat-text").append("<div class='panel panel-default'><div class='panel-heading'>" + $('<span/>').text(data.handle).html() + "</div><div class='panel-body'>" + $('<span/>').text(data.text).html() + "</div></div>");
  $("#chat-text").stop().animate({
    scrollTop: $('#chat-text')[0].scrollHeight
  }, 800);
};

The messages we’re using will be a JSON response with two keys: handle (user’s handle) and text (user’s message). When the message is received, it is parsed by JSON and inserted as a new entry in the page.

We override how the submit button works in our input form by using event.preventDefault() to stop the form from actually sending a POST. Instead, we grab the the values from the form and send them as a JSON message over the WebSocket to the server.

$("#input-form").on("submit", function(event) {
  event.preventDefault();
  var handle = $("#input-handle")[0].value;
  var text   = $("#input-text")[0].value;
  box.send(JSON.stringify({ handle: handle, text: text }));
  $("#input-text")[0].value = "";
});

Run locally

The sample application already contains a Procfile, which declares the web process:

web: go-websocket-chat-demo

We need to compile and install the executable into $GOPATH/bin:

$ go install -v

The Heroku CLI comes with the Heroku Local command to assist us in running the app locally, but it needs to know which $PORT and $REDIS_URL to connect to locally:

$ cp .env.local .env

Modify the .env file as necessary.

Let’s launch the web app locally.

$ heroku local
[OKAY] Loaded ENV .env File as KEY=VALUE Format

## Deploy

It’s time to deploy your app to Heroku. Create a Heroku app to deploy to:

```term
$ heroku create
Creating pure-river-3626... done, stack is heroku-18
Buildpack set. Next release on pure-river-3626 will use heroku/go.
https://pure-river-3626.herokuapp.com/ | https://git.heroku.com/pure-river-3626.git

Add a Redis add-on:

$ heroku addons:create heroku-redis
Creating flowing-subtly-2327... done, (free)
Adding flowing-subtly-2327 to pure-river-3626... done
Setting HEROKU_REDIS_CHARCOAL_URL and restarting pure-river-3626... done, v15
Database has been created and will be available shortly
Use `heroku addons:docs heroku-redis` to view documentation.

Deploy your code with git push.

$ git push heroku master
Counting objects: 624, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (342/342), done.
Writing objects: 100% (624/624), 808.36 KiB | 0 bytes/s, done.
Total 624 (delta 231), reused 611 (delta 225)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Go app detected
remote: -----> Checking vendor/vendor.json file.
remote: -----> Using go1.7
remote: -----> Installing govendor v1.0.3... done
remote: -----> Fetching any unsaved dependencies (govendor sync)
remote: -----> Running: go install -v -tags heroku .
remote: github.com/heroku-examples/go-websocket-chat-demo/vendor/github.com/Sirupsen/logrus
remote: github.com/heroku-examples/go-websocket-chat-demo/vendor/github.com/garyburd/redigo/internal
remote: github.com/heroku-examples/go-websocket-chat-demo/vendor/github.com/garyburd/redigo/redis
remote: github.com/heroku-examples/go-websocket-chat-demo/vendor/github.com/gorilla/websocket
remote: github.com/heroku-examples/go-websocket-chat-demo/vendor/github.com/heroku/x/redis
remote: github.com/heroku-examples/go-websocket-chat-demo/vendor/github.com/pkg/errors
remote: github.com/heroku-examples/go-websocket-chat-demo/vendor/github.com/satori/go.uuid
remote: github.com/heroku-examples/go-websocket-chat-demo
remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote:
remote: -----> Compressing...
remote:        Done: 2.4M
remote: -----> Launching...
remote:        Released v5
remote:        https://go-websocket-chat-demo.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/go-websocket-chat-demo.git
 * [new branch]      master -> master

Congratulations! Your web app should now be up and running on Heroku.

Next steps

Now that you have deployed your app and understand the basics we’ll want to expand the app to be a bit more robust. This won’t encompass everything you’d want to do before going public, but it will get you along the a path where you’ll be thinking about the right things.

Security

Remember, this is only a demo application and is likely vulnerable to various attacks. Please refer to WebSockets Security for more general guidelines on securing your WebSocket application.

Keep reading

  • Go

Feedback

Log in to submit feedback.

Information & Support

  • Getting Started
  • Documentation
  • Changelog
  • Compliance Center
  • Training & Education
  • Blog
  • Support Channels
  • Status

Language Reference

  • Node.js
  • Ruby
  • Java
  • PHP
  • Python
  • Go
  • Scala
  • Clojure
  • .NET

Other Resources

  • Careers
  • Elements
  • Products
  • Pricing
  • RSS
    • Dev Center Articles
    • Dev Center Changelog
    • Heroku Blog
    • Heroku News Blog
    • Heroku Engineering Blog
  • Twitter
    • Dev Center Articles
    • Dev Center Changelog
    • Heroku
    • Heroku Status
  • Github
  • LinkedIn
  • © 2025 Salesforce, Inc. All rights reserved. Various trademarks held by their respective owners. Salesforce Tower, 415 Mission Street, 3rd Floor, San Francisco, CA 94105, United States
  • heroku.com
  • Legal
  • Terms of Service
  • Privacy Information
  • Responsible Disclosure
  • Trust
  • Contact
  • Cookie Preferences
  • Your Privacy Choices