Go Websocket Tutorial

Elliot Forbes Elliot Forbes ⏰ 8 Minutes 📅 Jan 21, 2022

Hi Fellow Devs! In this tutorial, we are going to be looking at how we can use WebSockets within our own Go-based programs to do some cool real-time stuff.

By the end of this tutorial, we should have covered the following:

  • What WebSockets are
  • How we can build simple WebSocket Applications in Go

For the purpose of this tutorial, we’ll be working with the gorilla/websocket package as I’ve used it personally in a few production applications to great success.

Video Tutorial

WebSockets - What Are They?

So I’ve covered this a few times in a number of different tutorials, but it’s always worth mentioning why we use WebSockets and how they differ from traditional HTTP requests.

WebSockets are upgraded HTTP connections that live until the connection is killed by either the client or the server. It’s through this WebSocket connection that we can perform duplex communication which is a really fancy way of saying we can communicate to-and-from the server from our client using this single connection.

The real beauty of WebSockets is that they use a grand total of 1 TCP connection and all communication is done over this single long-lived TCP connection. This drastically reduces the amount of network overhead required to build real-time applications using WebSockets as there isn’t a constant polling of HTTP endpoints required.

A Simple Example

Let’s start off with a really simple Go program, once we can run that we can build on top of it and start building some simple WebSocket endpoints.

We’ll need to first initialize our project to use go modules by calling:

$ go mod init github.com/elliotforbes/go-websocket-tutorial

This command should create a go.mod file within our current directory.

Once this has been done, we can go ahead and define our main.go file in which we’ll do the majority of our coding:

main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello World")
}

Let’s sanity check that we can run this by opening up a terminal, navigating to our project’s directory and then calling go run main.go. We should see that it successfully outputs Hello World in our terminal.

A Simple HTTP Endpoint

We’re going to start off by building a simple HTTP server that returns Hello World whenever we hit it on port 8080. We’ll also define a simple HTTP endpoint that will act as the base of the WebSocket endpoint that we’ll be creating:

package main

import (
    "fmt"
    "log"
    "net/http"
)

func homePage(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Home Page")
}

func wsEndpoint(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

func setupRoutes() {
    http.HandleFunc("/", homePage)
    http.HandleFunc("/ws", wsEndpoint)
}

func main() {
    fmt.Println("Hello World")
    setupRoutes()
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Awesome, when we try run this using go run main.go we should see that it successfully starts up our newly defined HTTP server on http://localhost:8080 and we should subsequently be able to hit both the / and the /ws routes within our browser.

Upgrading a HTTP Connection

In order to create a WebSocket endpoint, we effectively need to upgrade an incoming connection from a standard HTTP endpoint to a long-lasting WebSocket connection. In order to do this, we are going to be using some of the functionality from the very cool gorilla/websocket package!

Define our Upgrader

The first thing we’ll have to do is to define a websocker.Upgrader struct. This will hold information such as the Read and Write buffer size for our WebSocket connection:

// We'll need to define an Upgrader
// this will require a Read and Write buffer size
var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

Check Incoming Origin

The next thing we’ll want to add to our existing wsEndpoint function is a call to upgrader.CheckOrigin. This will determine whether or not an incoming request from a different domain is allowed to connect, and if it isn’t they’ll be hit with a CORS error.

func wsEndpoint(w http.ResponseWriter, r *http.Request) {
    // remove the previous fmt statement
    // fmt.Fprintf(w, "Hello World")
    upgrader.CheckOrigin = func(r *http.Request) bool { return true }

}

For now, we have kept it really simple and simply return true regardless of what host is trying to connect to our endpoint.

Upgrading our Connection

We can now start attempting to upgrade the incoming HTTP connection using the upgrader.Upgrade() function which will take in the Response Writer and the pointer to the HTTP Request and return us with a pointer to a WebSocket connection, or an error if it failed to upgrade.

func wsEndpoint(w http.ResponseWriter, r *http.Request) {
    upgrader.CheckOrigin = func(r *http.Request) bool { return true }

    // upgrade this connection to a WebSocket
    // connection
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println(err)
    }

}

Continually Listening On That Connection

Next, we’ll want to implement a function which will continually listen for any incoming messages sent through that WebSocket connection. We’ll call this reader() for now and it will take in a pointer to the WebSocket connection that we received from our call to upgrader.Upgrade:

// define a reader which will listen for
// new messages being sent to our WebSocket
// endpoint
func reader(conn *websocket.Conn) {
    for {
    // read in a message
        messageType, p, err := conn.ReadMessage()
        if err != nil {
            log.Println(err)
            return
        }
    // print out that message for clarity
        fmt.Println(string(p))

        if err := conn.WriteMessage(messageType, p); err != nil {
            log.Println(err)
            return
        }

    }
}

With this defined, we can then add it to our wsEndpoint function like so:

func wsEndpoint(w http.ResponseWriter, r *http.Request) {
    upgrader.CheckOrigin = func(r *http.Request) bool { return true }

    // upgrade this connection to a WebSocket
    // connection
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println(err)
    }
    // helpful log statement to show connections
    log.Println("Client Connected")

    reader(ws)
}

With all of this in place, we should now be able to run our new WebSocket server like so:

$ go run main.go
Hello World

Awesome, everything appears to have worked!

Writing Back to Our Client

I mentioned before that WebSockets allow for duplex communication, i.e. back-and-forth communication across the same TCP connection. In order to send messages from our Go application to any connected WebSocket clients, we can utilize the ws.WriteMessage() function like so:

func wsEndpoint(w http.ResponseWriter, r *http.Request) {
    upgrader.CheckOrigin = func(r *http.Request) bool { return true }

    // upgrade this connection to a WebSocket
    // connection
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println(err)
    }

    log.Println("Client Connected")
    err = ws.WriteMessage(1, []byte("Hi Client!"))
    if err != nil {
        log.Println(err)
    }
    // listen indefinitely for new messages coming
    // through on our WebSocket connection
    reader(ws)
}

This addition means that any client that connects to our WebSocket server will be greeted by a nice Hi Client! message!

Testing it All Works With A Client

The final step, is testing to see if everything works by creating a client and attempting to connect to our new WebSocket endpoint. For this, we’ll create an incredibly simple vanilla JavaScript app that will connect on ws://localhost:8080/ws and attempt to send a message through this new WebSocket connection:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Go WebSocket Tutorial</title>
  </head>
  <body>
    <h2>Hello World</h2>

    <script>
        let socket = new WebSocket("ws://127.0.0.1:8080/ws");
        console.log("Attempting Connection...");

        socket.onopen = () => {
            console.log("Successfully Connected");
            socket.send("Hi From the Client!")
        };
        
        socket.onclose = event => {
            console.log("Socket Closed Connection: ", event);
            socket.send("Client Closed!")
        };

        socket.onerror = error => {
            console.log("Socket Error: ", error);
        };

    </script>
  </body>
</html>

There are a few ways you can serve this page locally. The quickest method I tend to use for validation is to use an npm library called live-server:

$ npm install -g live-server
$ live-server

This will automatically open a window in my preferred browser and serve any of the files in the current directory which is very nice.

Now, with our frontend now up and running, we can open up the browser’s console tools and within the console we should see that it has been able to successfully connect to our WebSocket server!

We should also see in our server logs that a client has successfully connected as well as a Hi From The Client! message!

We have now achieved full duplex communication!

Docker-izing our Application

I’ve talked about the benefits of Docker in a number of my other tutorials, but essentially, it allows us to define the exact environment that our App needs to run successfully. This means that you should be able to run this tutorial in 10 years time and it should still work perfectly.

FROM golang:1.11.1-alpine3.8
RUN mkdir /app
ADD . /app/
WORKDIR /app
RUN go mod download
RUN go build -o main ./...
CMD ["/app/main"]

Now that we have this Dockerfile defined for our application, let’s create the image using the docker build command and then let’s attempt to run this Go WebSocket application within a Docker container.

$ docker build -t go-websocket-tutorial .
$ docker run -it -p 8080:8080 go-websocket-tutorial

If everything has worked successfully, we should then be able to see our application up and running within our docker container mapped to our local machines port 8080. If we open up http://localhost:8080 within our browser, we should see our application return home page.

Conclusion

In this tutorial, we have managed to cover some of the basics of WebSockets and how you can build a simple WebSocket based application in Go!

So, hopefully you enjoyed this tutorial and found it useful! I’m hoping this highlighted some of the main benefits of using WebSockets within your own Go applications!

Source Code - The full source code for this tutorial can be found here: TutorialEdge/Go

Further Reading

If you enjoyed this article, you may like my tutorial series which utilizes WebSockets to build a Real-Time chat application using both React and Golang. It covers how you would pool WebSocket connections and do things such as broadcast updates to all connected clients!

Note - If you want to keep track of when new Go articles are posted to the site, then please feel free to follow me on twitter for all the latest news: @Elliot_F.