Run Go In The Browser Using WebAssembly

Estimated reading time: 8 Minutes

I’ve heard about WebAssembly a while back at one of the GopherCon conferences. It has been a source of interest to me and I’ve been meaning to play around with it for a while – and now, the time has come!

WebAssembly

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.

https://webassembly.org/

The short explanation I usually give people is that WebAssembly (or in short – WASM) is a way to run backend code in the browser. I know it’s not the most accurate explanation so let’s refine it.

WebAssembly is a new type of code. You don’t write code in WebAssembly, you compile your code to it. It allows you to compile code written in C, C++, Rust, Go and others and run it on the web (to begin with) in near native performance. This is great for video editing, gaming, music streaming and other applications that JavaScript will struggle with (performance wise)

So will it replace JavaScript? in short – no. These two, WebAssembly and JavaScript, go hand in hand. They are supposed to complement each other.

Today, all of the major browsers support WebAssembly out of the box, so no plugins are needed.

Why Use WebAssembly?

  1. First reason is the one we already spoke about – performance.
  2. Porting existing apps to the web – you can “simply” compile existing code to WebAssembly.
  3. Write web applications in your favorite language.
  4. Generally – use the same coding language for your frontend and backend.
  5. Easier to test you code (UTs in Go for example) and you need to test only one part of your of code (don’t need to test both server and client side).
  6. It’s securer by design.

Let’s Start Experimenting

For the purpose of this blog post I wanted to create a very simple and basic application, one might call it a POC.

Note: WebAssembly support was first introduced @ go v1.11. I will be using v1.14.

What’s The Plan?

Before starting: all of the code presented in this post is available here: https://github.com/omri86/intro-wasm-go

You’re going to create a simple application that encodes a given string to base64. So basically transforming the input string:

To make this happen, you’re going to create a web server (written in Go, of course) that will serve these files:

The explanation on how to create these files will follow in the next section.

1. index.html + JavaScript code

First part is the frontend: the web page and complementing JavaScript code. As seen in the image above, this won’t be anything fancy – a simple input & output textboxes and a encode button.

The JavaScript code will be part of the index.html file as well, and will handle the followingt tasks:

  1. Import the Go WebAssembly module
  2. Call functions in the Go WebAssembly module

2. encode.wasm

This “.wasm” file will be the WebAssembly binary that will contain the Go to JavaScript exposure and implementation of the base64 encoding. This file will be the compilation output of a “.go” file.

3. wasm_exec.js (AKA JavaSciprt Glue Code)

The JavaScript glue code is not something you need to create, only copy from your Go home library.

In short, part of the glue code is implementing the functionality of each respective library used by the Go code. The glue code also contains the logic for calling the WebAssembly JavaScript APIs to fetch, load and run the .wasm file.

Exposing Go To JavaScript

You’ll start with creating the Go file that will expose the Go base64 encoding function to JavaScript.

First off is the main function:

func main() {
	js.Global().Set("encode", encodeWrapper())
	<-make(chan bool)
}

You will be using package syscall/js which gives access to the WebAssembly environment when using the js/wasm architecture. Note that this package is still marked as experimental.

Line#2: set a JavaScript function called encode on the JavaScript scope from a Go function: encodeWrapper() (which you’ll see below).

Line#3: wait for the program to complete. Without this line your code will exit immediately.

Next part is the encoding function:

func encodeWrapper() js.Func {
	return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		if len(args) == 0 {
			return wrap("", "Not enough arguments")
		}
		input := args[0].String()
		return wrap(base64.StdEncoding.EncodeToString([]byte(input)), "")
	})
}

Line#1: as you can see this function returns a js.Func which, as the name suggests is a JavaScript function.

Line#2: this part might need a deeper explanation:

  1. It returns a js.FuncOf function which returns a wrapped js.Func.
  2. Invoking the JavaScript function will synchronously call the Go function defined inline.
  3. The function argument this is the JavaScript global object, whereas the args slice is the function invocation arguments.

Line#3: validate that sufficient amount of arguments were received. You can see that the return value is “warpped”, we’ll talk about this in a bit.

Line#6: take the first argument in the form of a string. This is the string to be encoded.

Line#7: encode the given string to base64 and return it.

Return Value From Go To JavaScript

The return value of the invocation is the result of the Go function mapped back to JavaScript.

Since JavaScript doesn’t “know” Go structs, we need to return a value that JavaScript understands. In our case the wrap function returns a map:

func wrap(encoded string, err string) map[string]interface{} {
	return map[string]interface{}{
		"error":  err,
		"encoded": encoded,
	}
}

This way, the JavaScript code can simply access the map values. Validate if an error occurred and display the resulting encoded value. You will see this code in the next section.

Putting it all together, this is our main.go file:

package main

import (
	"encoding/base64"
	"syscall/js"
)

func main() {
	js.Global().Set("base64", encodeWrapper())
	<-make(chan bool)
}

func encodeWrapper() js.Func {
	return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		if len(args) == 0 {
			return wrap("", "Not enough arguments")
		}
		input := args[0].String()
		return wrap(base64.StdEncoding.EncodeToString([]byte(input)), "")
	})
}

func wrap(encoded string, err string) map[string]interface{} {
	return map[string]interface{}{
		"error":   err,
		"encoded": encoded,
	}
}

Compiling Go To WebAssembly

Assuming you have Go installed, creating the encoded.wasm file is straightforward:

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

This will be the first of three files your backend will serve.

Copying The JavaScript Glue Code

As I’ve mentioned before, the glue code exists in your GOROOT folder, you can simply copy it by using the following command:

$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .

This is the second file your backend will serve. The last one is the frontend index.html file presented next.

Invoking Go From JavaScript

Your final part is the frontend code. Create a file called index.html and let’s go over the details line by line.

First thing you have to do is import the JavaScript glue code:

<script src="wasm_exec.js"></script>

Now you need to import the Go WebAssembly file to your JavaScript scope:

const go = new Go();
WebAssembly.instantiateStreaming(fetch("encode.wasm"), go.importObject).then(result => {
    go.run(result.instance);
}););

The WebAssembly.instantiateStreaming() function compiles and instantiates a WebAssembly module directly from a streamed underlying source. This is the most efficient, optimized way to load wasm code.

And now that we have the Go function as part of our JavaScript scope, we can invoke it:

let encodeJs = function(input) {
    let result = base64(input);
    if (result.error !== '') {
        alert(result.error);
    } else {
        output.value = result.encoded;
    }
}

The base64(input) function is the actual Go function. Pretty cool ha?!

Next you check for an error and set the output textbox value.

The complete HTML + JavaScript code looks like this:

<html>
    <head>
        <script src="wasm_exec.js"></script>
        <script>
            const go = new Go();
            WebAssembly.instantiateStreaming(fetch("encode.wasm"), go.importObject)
                .then(result => {
                go.run(result.instance);
            });
        </script>
    </head>
    <body style="text-align-last: center">
        <div>
            <textarea id="input" cols="40" rows="10">
            </textarea>
        </div>
        <div>
            <input type="submit"
                   value="> Base64 Encode <"
                   onclick="encodeJs(input.value)"/>
        </div>
        <div>
            <textarea id="output" cols="40" rows="10">
            </textarea>
        </div>
    </body>
    <script>
        let encodeJs = function(input) {
            let result = base64(input);
            if (result.error !== '') {
                alert(result.error);
            } else {
                output.value = result.encoded;
            }
        }
    </script>
</html>

Go Backend Code

Last step is to serve these files to the browser, lucky for us, creating an HTTP server in Go is a oneliner:

package main

import (
	"net/http"
)

func main() {
	panic(http.ListenAndServe(":9090", http.FileServer(http.Dir("."))))
}

Note the the file server directory that you point the server to needs to contain all 3 files mentioned in previous sections.

Run the server:

$ go run server.go

Open you browser at: http://127.0.0.1:9090 to see the magic happen.

Open Questions

As I was writing this blog post, I couldn’t stop thinking about these questions:

  1. Does WebAssembly really performs much better than JavaScript?
    Well, to figure that out I’ll need to create a benchmark and actually test this in depth. I’ll get to this in a follow up post.
    UPDATE: I’ve published Go WebAssembly Performance Benchmark.
  2. How much does a WebAssembly file weighs? does it make sense to serve clients with these file sizes?
    The encoded.wasm file mentioned above weighs about 2MB, I really do think it’s too much. I decided to dig deeper into the possibilities of making Go .wasm files smaller, this will also be published in another follow up post.
    UPDATE: I’ve published Minimizing Go WebAssembly Binary Size.

As you can see WebAssembly is a really exciting and interesting technology. I found myself intrigued by other aspects which I hope to investigate and post my findings in this blog.

Feel free to comment, like or share at will and contact me if you have any questions.

Stay tuned! see you on the next one.

Leave a Reply