Future of GopherJS and Go in the browser

Dmitri Shuralyov
GopherJS
Published in
5 min readDec 14, 2018

This post compares GopherJS to Go 1.11 and its new experimental WebAssembly port, suggests how Web API binding libraries can evolve to support both compilers, and provides context on the history and the future possibilities of Go in the browser.

Background and Future

Go 1.11 and GopherJS 1.11–1 mark an important and exciting milestone in the journey of Go in the browser. Go 1.11 added an experimental port to WebAssembly, giving Go programmers a second choice of a tool for compiling Go code into a format supported by modern web browsers. It was contributed to the Go project by Richard Musiol, the creator of GopherJS itself.

Richard started the GopherJS project in August of 2013 with the purpose of giving programmers the possibility of writing frontend code in Go. At that time, the major browsers provided only one¹ choice of an executable format: the JavaScript language. GopherJS had no choice but to use it as its compilation target.

GopherJS started out being very simple, and supported a handful of Go expressions, but then grew into an advanced Go compiler that supported nearly everything of the Go language specification, including goroutines, channels, and the select statement.

WebAssembly is a modern binary format designed specifically as a compilation target for high-level languages. It is still in early stages of development with many planned post-MVP future improvements, and it has the potential for reaching performance and binary size efficiency that surpasses what’s possible when compiling into JavaScript.

Having two working Go compilers that can target browsers is good for the Go ecosystem health. This is akin to gc and gccgo, the two backend Go compilers. It creates new opportunities, and helps discover or work around issues. At this time, there will be some programs and use cases that perform better when compiled with Go 1.11 to WebAssembly, and others where GopherJS does better.

Browser API Binding Packages

Package authors who have created bindings for various web APIs previously targeted only the GopherJS compiler and github.com/gopherjs/gopherjs/js package for calling into JavaScript. With Go 1.11, there is a new experimental syscall/js package in the standard library for the same purpose, which offers a similar but not identical API.

Authors of existing browser API bindings can add support for WebAssembly. Doing so is very helpful, as it allows users to write Go programs that can be compiled with both GopherJS to JavaScript and with Go 1.11 to WebAssembly. There are a few ways to accomplish this, described below.

Consider the following example Go binding for a small subset of the DOM Web API. It starts by targeting only GopherJS, like so:

// +build js// Package window is a binding for a tiny subset of the DOM Web API,
// concerned with the Window interface as documented at
// https://developer.mozilla.org/en-US/docs/Web/API/Window.
package window
import "github.com/gopherjs/gopherjs/js"// Alert displays an alert dialog with the specified message
// and an OK button. See its reference at
// https://developer.mozilla.org/en-US/docs/Web/API/Window/alert.
func Alert(message string) {
js.Global.Call("alert", message)
}

We’ll use this simple library to demonstrate different approaches to add WebAssembly support.

Using build tags

One of the Go proverbs states:

Syscall must always be guarded with build tags.

The github.com/gopherjs/gopherjs/js and syscall/js packages are no exception. We can use build tags (aka build constraints) to guard their use.

The architecture section of the GopherJS README notes:

The GOARCH value of GopherJS is js. You may use it as a build constraint: // +build js.

Go 1.11 uses GOOS=js and GOARCH=wasm values for WebAssembly. That means the // +build js build constraint will match both, and we need to use more specific // +build js,!wasm build constraint to differentiate code for GopherJS from code for WebAssembly. It looks like this:

// +build js,!wasm// Package window ...
package window
import "github.com/gopherjs/gopherjs/js"// Alert displays ...
func Alert(message string) {
js.Global.Call("alert", message)
}

The code above will be used when compiling package window with GopherJS. For WebAssembly, another .go file is added with // +build js,wasm build constraint:

// +build js,wasm// Package window ...
package window
import "syscall/js"// Alert displays ...
func Alert(message string) {
js.Global().Call("alert", message)
}

Note that it has a different build constraint, imports syscall/js, and uses its slightly different API.

This approach requires more maintenance work from the library authors, but offers the greatest flexibility and least overhead. It also precludes the wrapper from leaking the internal js details into its public API.

Using the gopherwasm wrapper

Another approach is to use the github.com/gopherjs/gopherwasm/js wrapper. This library abstracts over the github.com/gopherjs/gopherjs/js and syscall/js APIs, and presents a syscall/js-like API that is supported both by GopherJS and Go 1.11 WebAssembly.

To add WebAssembly support to our original example GopherJS binding, we need only to modify the single .go file to import gopherwasm and use its API:

// +build js// Package window ...
package window
import "github.com/gopherjs/gopherwasm/js"// Alert displays ...
func Alert(message string) {
js.Global().Call("alert", message)
}

This approach offers the benefit of there being only one .go file to maintain for the library authors. However, it adds some indirection and doesn’t allow to continue to rely on peculiarities of the github.com/gopherjs/gopherjs/js API that are not supported by gopherwasm (e.g., js struct field tags and what’s described here).

***

These two are the most common approaches that are coming up in practice so far. You can find some real examples of WebAssembly support being added at:

There may be other approaches to consider, such as creating distinct packages for GopherJS and WebAssembly, and more. You’ll need to find what works best for your needs.

In general, adding WebAssembly support is more viable for high-level GopherJS bindings that provide an idiomatic Go API and not expose the *js.Object details in the public API. This is a good practice when you think about the js packages as equivalent to other syscall packages: it’s better to avoid leaking such low-level details into public APIs for users to deal with.

Conclusion

Go 1.11 support for WebAssembly brings Go in the browser to a bigger audience, and pushes the state of technology forward in this exciting new space.

It’s a great time to build new things, experiment, find out what’s possible and works well, in order to advance the state of Go in the browser. Share what you create with your fellow gophers. Good luck and have fun!

¹ At that time, asm.js was just announced and beginning to appear in some browsers. It was too early to start targeting it right away.

--

--