Writing about Cloud, architecture, AWS, GCP and software engineering.

Golang WebAssembly

April 22, 2022
Source code: GitHub

About WebAssembly

WebAssembly (WASM) is a portable binary instruction format which runs in the browser or on a server. It is designed with performance and security in mind. WebAssembly can be compiled from other programming languages like C/C++, C#, Rust, Go and many more.

The goal was to complement JavaScript and not replace it. WebAssembly is meant to execute heavy and intensive compute task of a web applications. Which allows JavaScript to return focus on handling the browser interactivity and let WebAssembly do the heavy lifting.

WASM was originally created for the web, it has a lot of use cases think about image/video editing, games, VR, simulations and more.

WASM is starting to show up outside the browser thanks to WebAssembly System Interface (WASI). WASM+WASI has a lot of potential, see the tweet from Solomon Hykes, Co-founder of Docker.

WebAssembly is used by applications like AutoCad and Figma for a long time already.

Golang WebAssembly

WebAssembly can be compiled by Golang itself or with TinyGo. At the time of writing Go version 1.18 is the latest version and does not support WASI, if you wish to compile to WASI you need to use TinyGo.

How to use WebAssembly with Go in the browser

We will be making a simple Go hashing program, were we can SHA512 hash values with Go in the browser.

Let’s start with the directory structure:

go-wasm-example
    ├── static
    └── cmd
        ├── server
        └── wasm

Create the main Go file

Create a main.go file in go-wasm-example/cmd/wasm and add the following code to it:

package main

import (
	"crypto"
	_ "crypto/sha512"
	"encoding/hex"
	"fmt"
	"syscall/js"
)

func main() {
	done := make(chan struct{}, 0)
	js.Global().Set("wasmHash", js.FuncOf(hash))
	<-done
}

func hash(this js.Value, args []js.Value) interface{} {
	h := crypto.SHA512.New()
	h.Write([]byte(args[0].String()))

	return hex.EncodeToString(h.Sum(nil))
}

Let’s break the main function down.

The code done := make(chan struct{}, 0) & <-done is a Go channel, a channel waits for data to be sent to it and is used to keep the program running.

The js.Global().Set("wasmHash", hash) function will create a global JS function exposing the Go hash function to JavaScript.

In the hash function you see (this js.Value, args []js.Value) the argument from JS are passed as an array. To type cast the value from the argument you use .String() or .Int() you can read more about this in the syscall/js docs

Compile it to a .wasm file

To be able to compile it to WASM you need to add GOOS=js GOARCH=wasm to the build command. This tells Go to compile to a .wasm file.

GOOS=js GOARCH=wasm go build -o static/main.wasm cmd/wasm/main.go

Now you will see the main.wasm file in the static directory.

Supporting wasm_exec.js

The wasm_exec.js is provided by Go and is used to fetch and execute the main.wasm Go code in the browser.

The JavaScript file can be found in the GOROOT folder. To copy it to the static directory use the following command:

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

Create the index.html

We now have the .wasm binary and the wasm_exec.js code. Which we are going to load into an HTML page.

Let’s create the index.html file in the static directory.

Add the following code to the index.html:

<html>
<head>
    <meta charset="utf-8"/>
    <script src="wasm_exec.js"></script>
    <script>
        const go = new Go();
        WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
            go.run(result.instance);
        });
    </script>
</head>
<body>
<div>
    <label for="inputField">Enter value</label>
    <input id="inputField" name="Hash" type="text">
    <div id="outputHash" style="font-size: 20px"></div>
</div>
<script>
    var inputField = document.querySelector('#inputField')
    var outputHash = document.querySelector('#outputHash')
    inputField.addEventListener('keyup', function() {
        outputHash.innerHTML = wasmHash(inputField.value) // The function 'wasmHash' is defined in the Go code
    });
</script>
</body>
</html>

In the head code you find the JavasScript to run the Go WASM code. More information about this can be found here: https://github.com/golang/go/wiki/WebAssembly#getting-started

<script src="wasm_exec.js"></script>
<script>
    const go = new Go();
    WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
        go.run(result.instance);
    });
</script>

Webserver

To be able to serve the HTML file you will need a webserver. So we will create a basic one, add the following code to cmd/webserver/main.go

package main

import (
	"log"
	"net/http"
)

func main() {
	fs := http.FileServer(http.Dir("./static"))
	http.Handle("/", fs)

	log.Println("Listening on http://localhost:3000/index.html")
	err := http.ListenAndServe(":3000", nil)
	if err != nil {
		log.Fatal(err)
	}
}

Now we are able to serve the HTML file we just created by running:

go run ./cmd/webserver/main.go

Navigate to http://localhost:3000/index.html

Now you can input some text value into the input field and Go will hash your value.

Conclusion

WebAssembly has the advantage to be portable in and outside the browser. It is a compiled language so catching bugs happens before run time. It works hand in hand with Javascript, and it built with security in mind. WebAssembly is not yet widely adopted, and WebAssembly compiled with Go has its limitations.

I think WebAssembly really shines when porting desktop applications to the browser. For example for applications written in a language like C/C++.

More information about Go WebAssembly can be found here

The source code of this blog can be found here: https://github.com/tiborhercz/go-wasm-example