For many years there has been the only way to write client-side logic for the web; JavaScript. WebAssembly provides another way, as a low-level language similar to assembly, with a compact binary format. Go, a popular open-source programming language focused on simplicity, readability, and efficiency, recently gained the ability to compile to WebAssembly.

Here we explore the possibilities with writing and compiling Go to WebAssembly, from installation of Go, to the compilation to WebAssembly, to the communication between JavaScript and Go.

If you’re not familiar with Go, please start with the Go documentation and introductory tutorial to get an overall understanding of the language.

What is WebAssembly?

WebAssembly, (sometimes abbreviated to WASM, or in its logo WA) is a new language that runs in the browser, alongside JavaScript. One of its interesting features is that it can become a target for compilation from multiple other languages. The benefit of Web Assembly is that it can run at near-native speeds, providing an environment to run intensive or performance critical code. The language is not designed to replace JavaScript but exists alongside with the ability to talk between the two.

At its heart, WebAssembly is a low-level language. It can be considered similar to traditional assembly, with granular memory management and a binary format that can be compiled as fast as it downloads to produce code that’s guaranteed to run at speeds close to native applications. It’s very low level and designed to be a target for compiler writers rather than written by hand. This makes it an excellent target for higher-level languages like C, C++, Rust, and now: Go.

Getting Up and Running with Go

WebAssembly as a compile target only became available in Go 1.11. Even if you have Go installed, it’s worth checking with go version, which will print out the installed version. The way in which we install Go will be dependent on your platform, and unfortunately, it is not possible to cover. Here as an example we install Go 1.11 on macOS and Ubuntu using bash.

One way to install on macOS is to use Homebrew, the macOS package manager, to get Go 1.11. The following commands should allow you to install Go:

echo 'export GOPATH="${HOME}/.go"' >>~/.bashrc
echo 'export GOROOT="$(brew --prefix golang)/libexec' >>~/.bashrc
echo 'export PATH="$PATH:${GOPATH}/bin:${GOROOT}/bin"' >>~/.bashrc
 
test -d "${GOPATH}" || mkdir "${GOPATH}"
source ~/.bashrc
 
brew install go
go version

Here we add the necessary environment variables, GOPATH, GOROOT to your PATH. Here GOPATH is the location of your workspace (where you write your Go code). GOROOT points to the directory where Go gets installed. The test -d command will check that the GOPATH exists, and if not will create it. The source line will rerun the anything in ~/.bashrc into the current shell, so you don’t need to restart. Let’s have a look at how you would do the same on Ubuntu:

echo 'export GOPATH=$HOME/go' >>~/.bashrc
echo 'export GOROOT=/usr/local/go' >>~/.bashrc
echo 'export PATH=$GOPATH/bin:$GOROOT/bin:$PATH' >>~/.bashrc
 
cd /tmp
wget https://dl.google.com/go/go1.11.linux-amd64.tar.gz
sudo tar -xvf go1.11.linux-amd64.tar.gz
sudo mv go /usr/local
 
source ~/.bashrc
go version

At the end, we should see the version printed, which should be 1.11.

Your First WebAssembly Program from Go Code

Go has strong opinions on where your code should be, the workspace which gets located at GOPATH. This path would be $HOME/go on Linux and Mac if you followed the directions above. First, we will create a workspace directory; this should be $HOME/go on Linux and Mac. To use a different workspace you can do so by setting the GOPATH environment variable.

Go also has strong opinions on how the workspace should get arranged. Let’s assume we’re going to make a package called webassembly. The $GOPATH directory should have a folder named src containing the package folder in webassembly. Finally, we are going to want to make a file with the following path: $GOPATH/src/webassembly/webassembly.go.

Next, for the actual code for this file we’ll write a simple ‘Hello World’ application:

package main
 
import "fmt"
 
func main() {
    fmt.Println("Hello, World!")
}

We can then compile this Go file to WASM using the following command:

GOOS=js GOARCH=wasm go build -o main.wasm

Here GOOS is the target ‘operating system’ which in this instance is JavaScript, and GOARCH is the architecture which in this case is WebAssembly (wasm). Running this command will compile the program to a file called `main.wasm` in the folder. You might notice the build size is quite large (~2mb). While this could be prohibitive depending on your application, this should significantly improve in the future as more work goes into the Go wasm target and the underlying wasm standards gain additional functionality.

Using Your Compiled WebAssembly in the Browser

To call our compiled Go code, we need to make use of a library provided in the Go source called wasm_exec.js. We can find this in the GOROOT folder under /misc/wasm/wasm_exec.js. The easiest thing to do here is simply copy-and-paste it to where we want to use our WASM file. For simplicity, let’s assume we’re going to write our index.html file (which hosts the code for our web page) where will load our WASM file, in the same folder as where we wrote our Go file ($GOPATH/src/webassembly/webassembly.go). Let’s go ahead and see what that would look like:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <script src="wasm_exec.js"></script>
        <script>
             
            if (WebAssembly) {
                 // WebAssembly.instantiateStreaming is not currently available in Safari
                 if (WebAssembly && !WebAssembly.instantiateStreaming) { // polyfill
                     WebAssembly.instantiateStreaming = async (resp, importObject) => {
                        const source = await (await resp).arrayBuffer();
                         return await WebAssembly.instantiate(source, importObject);
                     };
                 }  
 
                 const go = new Go();
                 WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
                    go.run(result.instance);
                 });
            } else {
               console.log("WebAssembly is not supported in your browser")
            }
 
        </script>
    </head>
    <body>
        <main id="wasm"></main>
    </body>
</html>

Here the wasm_exec.js file gives us access to the new Go() constructor which allows us to run our code. The WebAssembly global is accessible in supported browsers. We use the instantiateStreaming method, which is polyfilled for unsupported browsers (currently Safari out of the big four browser vendors). WebAssembly.instantiateStreaming is the most efficient way to load WASM code, compiling and instantiating a module for us to use. We can combine it with the fetch API to retrieve our WASM file. The result then gets passed to our constructed Go object that lets us run our compiled code.

Before we load our page and try out the code, we will need to run it on a web server to avoid CORS issues. Here I would recommend http-server, a Node.js package which provides a fully functioning web server. If you were determined to stay in the Go ecosystem to serve your files, the Go documentation recommends doing so using goexec by running goexec 'http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))' .

If we save this page, then open our developer console, we should see Hello, WebAssembly printed to out.

Interacting with JavaScript and the DOM

You can call JavaScript from WASM using the syscall/js module. Let’s assume we have a function in JavaScript simply called updateDOM that looks like this:

function updateDOM(text) {
    document.getElementById("wasm").innerText = text;
}

All this function does is set the inner text of our main container to whatever gets passed to the function. We can then call this function from our Go code in the following fashion:

package main
 
import (
    "syscall/js"
)
 
func main() {
    js.Global().Call("updateDOM", "Hello, World")
}

Here we use the js.Global function to get the global window scope. We call the global JavaScript function updateDOM by using Call method on the value returned from js.Global. We can also set values in JavaScript using the Set function. At the moment setting values works well with basic types but errors on types such as structs and slices. Here we’ll pass some basic values over to JavaScript, and show how you could use a simple workaround to marshal a struct into JSON by leveraging JavaScript’s JSON.parse.

package main
 
import (
    "encoding/json"
    "fmt"
    "syscall/js"
)
 
type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}
 
func main() {
 
    js.Global().Set("aBoolean", true)
    js.Global().Set("aString", "hello world")
    js.Global().Set("aNumber", 1)
 
    // Work around for passing structs to JS
    frank := &Person{Name: "Frank", Age: 28}
    p, err := json.Marshal(frank)
    if err != nil {
        fmt.Println(err)
        return
    }
    obj := js.Global().Get("JSON").Call("parse", string(p))
    js.Global().Set("aObject", obj)
 
}

We can also use Set to bind these values to callbacks within Go, using the NewCallback method. Let’s say we want to set a method in JavaScript, bind it to a Go function and make it call a method when it’s called. We could do that like this:

package main
 
import (
    "fmt"
    "syscall/js"
)
 
func sayHello(val []js.Value) {
    fmt.Println("Hello ", val[0])
}
 
func main() {
    c := make(chan struct{}, 0)
    js.Global().Set("sayHello", js.NewCallback(sayHello))
    <-c
}

Here we create a channel of length zero and then await values (which never arrive) keeping the program open. This allows the sayHello callback to get called. Assuming we had a button which calls the function entitled sayHello, this would, in turn, call the Go function with whatever argument gets passed in, printing the answer (i.e., ‘Hello, World’).

We can also use the Get method to retrieve a value from the JavaScript main-thread. For example, let’s say we wanted to get the URL of the current page. We could do that by doing the following:

import (
    "fmt"
    "syscall/js"
)
 
func main() {
    href := js.Global().Get("location").Get("href")
    fmt.Println(href)
}

Which would print out the webpage URL to the web console. We can extrapolate this to get hold of any global JavaScript object, like document or navigator for example.

Conclusion

In this post, we have seen how to get the version of Go necessary to compile WebAssembly, how to structure your files, and how to compile a simple Go program to WebAssembly. We have also taken this further and demonstrated how to set JavaScript variables from Go (and in turn the DOM), set Go variables from JavaScript, and also set Go callbacks in JavaScript.

The true value of WebAssembly here is to do heavy lifting operations that we may normally do in something like a Web Worker. There are a few examples of such programs across the web, including this A Star path search algorithm, calculating factorials, a fully fledged Gameboy Color emulator (written in Go), or this video effects application are good examples of where the near-native speeds of WebAssembly shine.

Generally, any time we are considering heavy computation in the browser WebAssembly may be a good choice. Unfortunately, as we have to proxy DOM updates to JavaScript, it is unlikely that DOM heavy code would see much benefit from using WebAssembly. Having said this, WebAssembly provides another tool in the web developer’s arsenal, allowing them to unlock near-native performance for certain tasks.