When it comes to building applications in WebAssembly, Rust and C++ are the two most frequently used languages at the moment. However, both languages have relatively steep learning curves, so it makes sense to also consider other options. Go is known for its simplicity and easy-to-use concurrency model. This makes it a great choice for a wide range of web oriented applications. In this article, we will explore how Go applications can target WebAssembly and what the interaction between JavaScript and Go looks like.
The example application
While WebAssembly has seen quite some hype in recent years, it seems that most blogs only focus on very simple applications, such as "hello world" or summing two integers. In my experience with WebAssembly in C++, the real fun only begins when working with dynamic memory, such as arrays or objects. However, we will see that Go handles even these cases very well.
The example application. Source image on the left by Ermell.
We will study a small example application for edge detection in images. The application makes use of the JavaScript File API to load an image file as a byte array. The byte array is sent to a WebAssembly module written in Go. The module applies a convolution filter and returns another image, which the JavaScript code will display with an <image>
element.
Basic application structure
Let us start with the basic scaffolding for a Go WebAssembly application:
1package main
2
3import "syscall/js"
4
5func applySobelOperator(this js.Value, args []js.Value) any {
6 // TBD
7}
8
9func main() {
10 js.Global().Set("applySobel", js.FuncOf(applySobelOperator))
11 <-make(chan bool)
12}
We aim to expose the function applySobelOperator
to JavaScript. The Go syscall/js
package provides support for this interaction. The type of the function must be:
1func(this js.Value, args []js.Value) any
This function type represents a JavaScript function invocation with the static type system of Go. The first parameter is this
, which will hold whatever the function was bound to in JavaScript. The second parameter, args
, is an array of the arguments, because in JavaScript, an arbitrary number and types of arguments can be passed to a function. We will see below how to work with those values. The function may return any Go type.
When the main method is executed, the WebAssembly instance will register the function in the global object of JavaScript, i.e. window
when running in the browser. The function can be invoked from JavaScript as long as the instance is still running. The easiest way to let the main method run forever is by waiting on an anonymous channel.
Go can be compiled to WebAssembly with the standard Go compiler, using
1$ GOOS=js GOARCH=wasm go build -o sobel.wasm .
Additionally, we have to serve the wasm_exec.js
bundled with the Go release. The file is the JS counterpart to the syscall/js
package. It provides a few JS implementations of this package and handles transferring data from/to the WebAssembly instance's memory. This script has to be executed before we can run the WebAssembly module.
1$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" wasm_exec.js
We run the .wasm
file from JavaScript like so:
1<!--...--> 2<body> 3 <div class="content"> 4 <!--...--> 5 </div> 6 <script src="wasm_exec.js"></script> 7 <script> 8 const go = new window.Go(); 9 10 WebAssembly.instantiateStreaming( 11 fetch("sobel.wasm"), 12 go.importObject 13 ).then( 14 (obj) => { 15 go.run(obj.instance); 16 } 17 ); 18 </script> 19</body> 20<!--...-->
Already with those small snippets of Go and HTML/JS, quite a few things are happening at runtime:
- Browser loads and renders the
.html
. - The
wasm_exec.js
is loaded and executed. It adds its functionality in a global classGo
. - The
<script>
-tag is executed, this will - fetch the
.wasm
file containing the WebAssembly byte code. WebAssembly.instantiateStreaming
will compile and instantiate the byte code into aWebAssembly.Instance
.- The instance is run, where
- our Go main function registers
applySobel
inwindow
, ready to be called.
Interaction between Go and JavaScript
For loading an image from disk and handing it over to the WebAssembly instance, we implement the change
event on an <input type="file">
element:
1fileInput.addEventListener("change", handleFiles);
2
3async function handleFiles() {
4 if (this.files.length) {
5 const file = this.files[0];
6 const arrayBuffer = await file.arrayBuffer();
7 const input = new Uint8Array(arrayBuffer);
8 const result = window.applySobel(input);
9 targetImage.src = URL.createObjectURL(new Blob([result]));
10 }
11}
The arrayBuffer()
method on the selected file already returns the raw binary data. We wrap it in a typed array view, to make it compatible with our Go code.
A central type defined in the syscall/js
package is js.Value
. This type can represent any JavaScript value. It provides methods to retrieve typed values, such as Int()
. In case our compile-time assumption on the type of the value is wrong, e.g. we call Int()
on a floating point value, the program crashes at runtime.
If the value is a JS object, it is also possible to call methods with Call(m string, args ...any)
, or retrieve properties with Get(p string)
.
Putting those together, we retrieve the length of the parameter, the byte array, with:
1args[0].Get("length").Int()
Go also has support for JS typed arrays. This is quite handy, because it allows us to transfer raw data from and to JavaScript.
Using CopyBytesToGo(dst []byte, src js.Value)
, we can copy the content of an Uint8Array
into a slice of type byte
.
It is also possible to create JavaScript objects, e.g. a new Uint8Array
of a certain size with:
1js.Global().Get("Uint8Array").New(size)
Creating a JavaScript object is especially useful when we want to return it from a function.
Here is the implementation of the Go function called in the above JavaScript extract:
1func applySobelOperator(this js.Value, args []js.Value) any {
2 inputBuffer := make([]byte, args[0].Get("byteLength").Int())
3 js.CopyBytesToGo(inputBuffer, args[0])
4 img, _, _ := image.Decode(bytes.NewReader(inputBuffer))
5
6 resultImage := sobelConvolution(img)
7
8 var outputBuffer bytes.Buffer
9 png.Encode(&outputBuffer, resultImage)
10 outputBytes := outputBuffer.Bytes()
11
12 size := len(outputBytes)
13 result := js.Global().Get("Uint8Array").New(size)
14 js.CopyBytesToJS(result, outputBytes)
15
16 return result
17}
The actual implementation of the sobel image convolution is omitted for brevity. The full implementation is available here.
Conclusion
Using Go to build a WebAssembly module is straight-forward. No separate toolchain is required. The standard library brings a package to handle interop between JavaScript and Go.
This power allows some flexibility on how to structure the application. For example, it would have been also possible to write the entire application in Go and perform DOM manipulation by means of something like
1js.Global() 2 .Get("document") 3 .Call("getElementById", "target") 4 .Set("property", value)
Another option would be to use the Canvas API to extract the raw RGBA channel of the image and only transfer that to WebAssembly.
However, the great interoperability between Go and JavaScript comes at a cost: Our Go module is dependent on JavaScript, which means it will not run in a non-JavaScript environment. That is what WASI, the WebAssembly System Interface, is about. With WASI, WebAssembly has the potential to alter the way we write and deploy applications. Go not being compliant with WASI is a big drawback. In an upcoming article, we will see how to respond to this and other challenges when using Go for WebAssembly.
More articles
fromJulian Arz
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog author
Julian Arz
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.