Using Go in the Browser via Web Assembly
I’ve wanted to try out Web Assembly for a while — it feels like it offers great potential for more flexibility in the browser. I’ve also been wanting to look at Go for some time now, so I decided to combine the two in one experiment. Here’s how I did in writing a simple web page, using Go as the programming language.
Putting the Structure in Place
Thinking about a simple web page to build, I settled on a simple form to do addition. It would have 3 boxes, with the third box updating to show the sum of the first two boxes. I started by creating a simple directory structure:
$ mkdir -p public/js
$ touch public/index.html
$ tree
.
└── public
├── index.html
└── js
2 directories, 1 file
tree
$
Then in the index.html
, I created a simple page:
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Golang Calculator</title>
</head>
<body>
<h1>Golang Calculator</h1>
<form id="calc">
<input type="text" id="first-number" size="2" />
+
<input type="text" id="second-number" size="2" />
=
<input type="text" id="result" size="2" readonly />
</form>
</body>
</html>
This gave me a starting point:
Even though it’s an experiment, I want it to look good, so now I’ll add in a
little bit of CSS
:
@import url("https://fonts.googleapis.com/css?family=Roboto:400,400italic,700,700italic");
body {
margin: 0px;
padding: 0px;
font-family: Roboto,Helvetica,Arial;
background: #fbfbfb;
color: #999;
text-align: center;
padding-top: 40px;
}
input {
border: 1px solid #999;
border-radius: 4px;
padding: 2px 4px;
color: #999;
}
.info {
color: blue;
border-color: blue;
background: lightskyblue;
}
.success {
color: green;
border-color: green;
background: palegreen;
}
.warning {
color: goldenrod;
border-color: goldenrod;
background: lemonchiffon;
}
.error {
color: red;
border-color: red;
background: pink;
}
form#calc, form#calc > input {
font-size: 20pt;
}
form#calc > input {
text-align: center;
}
Which gives me this:
Finally, I want to open the page in a loading state, so I’ll add in a DIV
with my loading text in it:
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Golang Calculator</title>
<link rel="stylesheet" href="style.css" type="text/css" media="screen" />
</head>
<body>
<h1>Golang Calculator</h1>
<div id="loading">Loading ...</div>
<form id="calc">
<input type="text" id="first-number" size="2" />
+
<input type="text" id="second-number" size="2" />
=
<input type="text" id="result" size="2" readonly />
</form>
</body>
</html>
And I’ll add a statement to my CSS
to hide the form initially:
form#calc {
display: none;
}
This gives me my initial page, with the form hidden:
Setting up a Web Server
So far, I’ve just opened this file locally. What I need is a server to provide
this content. For the purposes of this exercise, I will just create one in
Go. I’ll create a src
folder, and then put this file into it:
package main
import (
"log"
"net/http"
)
func main() {
log.Println("Server listening on port 3000")
http.ListenAndServe(":3000", http.FileServer(http.Dir("public/")))
}
Now I can just run this server:
$ go run src/server.go
2019/08/14 10:30:40 Server listening on port 3000
And I can now access my web page from an http
address:
Building my Browser-Based Go Program
Now I have everything in place, I can move on to look at writing the Go code
which will provide me the interactivity I need. I will create a file main.go
in my src
directory for this. The important thing I need is some way to
interact with my web page, or more accurately with the DOM
for my web page.
For this, I need to import
the syscall/js
module, which gives me a variable
js
I can interact with:
package main
import (
"syscall/js"
)
Using this, I can get access to the environment’s global variables (equivalent
to window
in the browser, or global
in Node). And from there, I can get
the browser’s document
object:
func main() {
document := js.Global().Get("document")
You can see in this example that I have used a Get()
function, which is the
way syscall/js
lets me retrieve values from objects. It also has a Call()
function which I can use to invoke methods on objects from the DOM
. The
standard way to find an element from a web page in Javascript, is to use the
getElementById()
method on the document
. So, in Javascript, I can find
my loading
element like this:
The equivalent in Go, using syscall/js
is:
document.Call("getElementById", "loading")
By combining Call()
and Get()
, I can build up an expression to hide the
loading message. In Javascript, it looks like this:
In Go, this becomes:
document.Call("getElementById", "loading").Get("style").Call("setProperty", "display", "none")
I can do something similar to display the form, so I end up with this code:
package main
import (
"syscall/js"
)
func main() {
document := js.Global().Get("document")
document.Call("getElementById", "loading").Get("style").Call("setProperty", "display", "none")
document.Call("getElementById", "calc").Get("style").Call("setProperty", "display", "block")
}
Now I need to build this code into a web assembly executable (a .wasm
file).
Standard compilation would look like this:
go build -o public/js/main.wasm src/main.go
However, this will just give me a standard executable. What I need to do is set
two environmental variables, so that it generates a WASM
file. The first
variable I need to set is the GOOS
variable, which lets Go know which
operating system I am targetting. In this case, I am targetting javascript
as
my operating system, so I need to set GOOS=js
. The second variable is the
GOARCH
variable, which sets the architecture. I need this to be web assembly,
so I need to set GOARCH=wasm
. So, my build step is:
GOOS=js GOARCH=wasm go build -o public/js/main.wasm src/main.go
I will also need a standard Javascript wasm
execution architecture for Go.
This is shipped with Go itself, so I can just copy that:
cp /usr/local/go/misc/wasm/wasm_exec.js public/js/
Now I have this set of files:
$ tree
.
├── public
│ ├── index.html
│ ├── js
│ │ ├── main.wasm
│ │ └── wasm_exec.js
│ └── style.css
└── src
├── main.go
└── server.go
3 directories, 6 files
tree
Finally, I need to include my web assembly file in my HTML
. The first part of
this is including the wasm_exec.js
file. This is simply Javascript, so I can
include as usual:
<script src="js/wasm_exec.js"></script>
Including the code I’ve written is slightly more complicated, because I need to
use the WebAssembly
module to load and run my web assembly code (we can’t
just include web assembly yet). The first step is to create an instance of the
Go
class, which is provided by wasm_exec.js
:
const go = new Go();
Now I need to do three things to run my web assembly:
- Get my
.wasm
file from the web server - Create a runnable instance of that file
- Run that instance
To get my .wasm
file, I can use the standard fetch()
call:
fetch("js/main.wasm")
I can then use WebAssembly’s instantiateStreaming()
function to convert this
to a runnable instance. This takes two arguments — wasm
code, and a context
to instantiate against. I already have the wasm
code from fetch()
(fortunately instantiateStreaming()
accepts the promise that fetch()
returns). I can get the context from my go
object, where it is stored under
importObject
. This means my code looks like this:
WebAssembly.instantiateStreaming(fetch("js/main.wasm"), go.importObject)
This returns a promise, which resolves to two parts — module
and
instance
. The module
contains the compiled code, and I can use that if I
need to instantiate again. The instance
is what I am interested in here,
since it contains the instance I need to run. I run this returned instance
using the run()
method from my go
object:
WebAssembly.instantiateStreaming(fetch("js/main.wasm"), go.importObject)
.then(result => { go.run(result.instance); });
Adding all the above into my HTML
file, I have this:
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Golang Calculator</title>
<link rel="stylesheet" href="style.css" type="text/css" media="screen" />
<script src="js/wasm_exec.js"></script>
<script>
go = new Go();
WebAssembly.instantiateStreaming(fetch("js/main.wasm"), go.importObject)
.then(result => { go.run(result.instance); });
</script>
</head>
<body>
<h1>Golang Calculator</h1>
<div id="loading">Loading ...</div>
<form id="calc">
<input type="text" id="first-number" size="2" />
+
<input type="text" id="second-number" size="2" />
=
<input type="text" id="result" size="2" readonly />
</form>
</body>
</html>
If I start my server and reload the page, I now get this:
I know this looks the same as what I had earlier, but if you actually try this out, you’ll see the page open with the loading message which then disappears and is replaced by the form.
So, I have functional code, but it’s not very readable. I’ll move the lower
level DOM
manipulation to its own package, then my main.go
can deal in
easier to understand high level concepts. To begin with, I’ll create a
packages
directory in my project, and create a dom
folder in there to take
my package:
$ tree
.
├── main
├── packages
│ └── src
│ └── dom
│ └── dom.go
├── public
│ ├── index.html
│ ├── js
│ │ ├── main.wasm
│ │ └── wasm_exec.js
│ └── style.css
└── src
├── main.go
└── server.go
6 directories, 8 files
tree
I’ll start the package by importing syscall/js
and then initialising the
document
variable so I can useit throughout the package:
package dom
import (
"syscall/js"
)
var document js.Value
func init() {
document = js.Global().Get("document")
}
Now I can use that document
variable to define a getElementById()
function:
func getElementById(elem string) js.Value {
return document.Call("getElementById", elem)
}
Building on that, I can now create a getElementValue()
function, using
getElementById()
:
func getElementValue(elem string, value string) js.Value {
return getElementById(elem).Get(value)
}
Finally, I can define a Hide()
and a Show()
function built on top of that:
func Hide(elem string) {
getElementValue(elem, "style").Call("setProperty", "display", "none")
}
func Show(elem string) {
getElementValue(elem, "style").Call("setProperty", "display", "block")
}
This allows me to write a much more easy to understand main.go
:
package main
import (
"dom"
)
func main() {
dom.Hide("loading")
dom.Show("calc")
}
To build this, I now need to add my packages
directory into my GOPATH
in my
build command:
GOOS=js GOARCH=wasm GOPATH=$PWD/packages go build src/main.go
If I run my webserver again and load this new version, it works exactly as it did before — it’s just more understandable.
Performing the calculation
The way I want the web page to work, is that changing the number in either of
the first two input fields automatically updates the third input field with the
result. If something non-numeric is entered into either field, I want the
message ERR
to be displayed in the result field, and I want that field to
change colour to indicate an error (by assigning my .error
CSS class to it).
Correcting the error should clear the styling and display the correct sum
result.
So, I am going to create a performCalculation()
function to actually do the
addition, and I am going to then attach that via an event listener to the input
fields, so it gets triggered every time an input value changes. Before I can do
that, I need to add some more functionality to my dom
package.
I want to be able to get and set a value from a DOM
element, so let’s add
those in:
func GetString(elem string, value string) string {
return getElementValue(elem, value).String()
}
func SetValue(elem string, key string, value string) {
getElementById(elem).Set(key, value)
}
And then I want to be able to add and remove CSS classes:
func AddClass(elem string, class string) {
getElementValue(elem, "classList").Call("add", class)
}
func RemoveClass(elem string, class string) {
classList := getElementValue(elem, "classList")
if (classList.Call("contains", class).Bool()) {
classList.Call("remove", class)
}
}
Going back to my main.go
program, I can now write a performCalculation()
function:
func performCalculation() {
firstNumber, firstNumberErr := strconv.Atoi(dom.GetString("first-number", "value"))
secondNumber, secondNumberErr := strconv.Atoi(dom.GetString("second-number", "value"))
if (firstNumberErr == nil && secondNumberErr == nil) {
dom.SetValue("result", "value", strconv.Itoa(firstNumber + secondNumber))
dom.RemoveClass("result", "error")
} else {
dom.SetValue("result", "value", "ERR")
dom.AddClass("result", "error")
}
}
What this function does is get the values from the first two fields, and
attempt to convert them to integer values (using strconv.Atoi()
). As long as
there are no errors, it adds together the two values, converts that to a string
and puts it into the result
field. If it does encounter a conversion error,
it sets the result
value to ERR
and also adds the .error
class to the
result field.
To check that everything is working as intended, I’ll set some values in the
first two field in my main()
function, then call performCalculation()
:
func main() {
dom.Hide("loading")
dom.Show("calc")
dom.SetValue("first-number", "value", "7")
dom.SetValue("second-number", "value", "5")
dom.SetValue("result", "value", "0")
performCalculation()
}
If I build and run this, I should now see the correct result in the browser:
I can also change one of the values to not be valid (for example X
), to test
my error display:
The final part is to add performCalculation()
as an event listener, so I’ll
add this capability to my dom
package. This gets a bit fiddly, so strap in
while I go through this. My function is simple — it takes no parameters and
returns no result. So it has the signature:
func()
The function I need to call in syscall/js
(which is called js.FuncOf()
)
needs a function with this signature:
func(js.Value, []js.Value) interface {}
So, I need to write a function to perform this conversion. It needs to accept a
function of the first signature, and return one with the second. I’ll call this
wrapGoFunction()
and the declaration looks like this:
func wrapGoFunction(fn func()) func(js.Value, []js.Value) interface {} {
The actual body is quite straightforward, it just returns a function with the
new signature, and in the body of that function it just calls our original
function. It also needs to return
a value, so it meets the required signature:
func wrapGoFunction(fn func()) func(js.Value, []js.Value) interface {} {
return func(_ js.Value, _ []js.Value) interface {} {
fn()
return nil
}
}
I still need to wrap the result of that in a call to js.FuncOf()
, so the full
AddEventListener()
function in my dom
package looks like this:
func AddEventListener(elem string, event string, fn func()) {
getElementById(elem).Call("addEventListener", event, js.FuncOf(wrapGoFunction(fn)))
}
Now I can make use of this in main.go
:
func main() {
dom.Hide("loading")
dom.Show("calc")
dom.SetValue("first-number", "value", "0")
dom.SetValue("second-number", "value", "0")
dom.SetValue("result", "value", "0")
dom.AddEventListener("first-number", "input", performCalculation)
dom.AddEventListener("second-number", "input", performCalculation)
}
This all looks good, but when I try it in the browser it doesn’t seem to work. Opening the console shows me the following error:
Okay, looks like my program executes and exits, so when I try to invoke my event handler, there’s no process to do that. So, I need some way to keep the program running. I’ll do this by creating a channel, waiting for an event on it, but never sending one:
func main() {
dom.Hide("loading")
dom.Show("calc")
dom.SetValue("first-number", "value", "0")
dom.SetValue("second-number", "value", "0")
dom.SetValue("result", "value", "0")
dom.AddEventListener("first-number", "input", performCalculation)
dom.AddEventListener("second-number", "input", performCalculation)
ch := make(chan struct{})
<-ch
}
Building this and running it in the browser, it now all works as expected:
Reducing the Size of the WASM File
Although this worked as I wanted, every now and again it would just not run when I reloaded the page. I could fix that by visiting another page and coming back, but it was a poor user experience. Opening up the console, I found this message about running out of memory:
Digging around a bit more, I found that my .wasm
file was 1.4M:
I don’t know if these two facts are related, but one obvious area to look at is
how to reduce the size of the .wasm
file. Compression was my first port of
call — that might help with transfer but I couldn’t see it helping with the
memory issue. So I started looking for alternative ways to build Go into a
smaller artefact. I found tinygo, which was developed for smaller devices
(e.g. IoT) but also can produce Web Assembly output. So I tried that, but got
an error:
$ GOPATH=./packages tinygo build -o public/js/main.wasm -no-debug src/main.go
error: async function dom.wrapGoFunction$1 used as function pointer
After doing some reading, it looks like tinygo doesn’t support function
pointers. So I need to take out my function which maps to the syscall/js
event listener function signature. Within my dom
package, this is quite
simple — I just need to change the signature of the function I pass in:
func AddEventListener(elem string, event string, fn func(js.Value, []js.Value) interface{}) {
getElementById(elem).Call("addEventListener", event, js.FuncOf(fn))
}
For main.go
it is slightly more complicated. Because the js.Value
type has
now leaked into my event listener, I need to import syscall/js
into
main.go
:
import (
"dom"
"strconv"
"syscall/js"
)
Now I need to update performCalculation()
to match the function signature.
Since I don’t need to do anything with the parameters, I can just use
underscores instead of parameter names:
func performCalculation(_ js.Value, _ []js.Value) interface{} {
firstNumber, firstNumberErr := strconv.Atoi(dom.GetString("first-number", "value"))
secondNumber, secondNumberErr := strconv.Atoi(dom.GetString("second-number", "value"))
if (firstNumberErr == nil && secondNumberErr == nil) {
dom.SetValue("result", "value", strconv.Itoa(firstNumber + secondNumber))
dom.RemoveClass("result", "error")
} else {
dom.SetValue("result", "value", "ERR")
dom.AddClass("result", "error")
}
return nil
}
Note that I also needed to return a value, so I used nil
since I am not doing
anything with the result. Now when I try to build, it does so successfully and
gives me a much smaller output file — down from 1.4M to 27K:
$ ls -lh public/js/main.wasm
-rw-r----- 1 ian ian 27.1K Aug 17 21:27 public/js/main.wasm
Note that each Go implementation has its own wasm_exec.js
, I can’t just keep
using the one I had earlier. So I need to also copy the tinygo-specific
version:
cp /usr/local/tinygo/targets/wasm_exec.js public/js/wasm_exec.js
Now if I start up my web server and go to my web page, I get the same display and functionality as before, except with a much smaller filesize:
Conclusions
In conclusion, this seems a promising way to build web frontends, but I’m not 100% convinced that I’d want to drop it into a large enterprise project right now. This is for a variety of reasons:
File Size
The file size is an obvious area of concern — either the standard Go compiler needs to produce smaller WASM files, or an alternative compiler needs to work with standard Go capabilities. Clearly this can be worked around as I did here, but for me this would be a fairly big limitation, since I tend to use function pointers and closures. Perhaps this is less of a constraint for other people (and perhaps my Go is not so idiomatic).
Web Assembly as a Second Class Citizen
Not a big deal, but it seems to me that Web Assembly would be more easily used
if it could be added directly via SCRIPT
tags, rather than needing a bit of
Javascript to glue it into place. This is not specifically a Go issue, but more
of a Web Assembly issue.
Availability of Go Developers
This may be a plus or a minus point. Generally, it is easier for me to find Javascript developers than Go developers. If I needed to staff a frontend team, then choosing Go as my language would add another hurdle for me. Where I can see this being a positive point, would be if the backend is already being developed in Go. Then I would already have a pool of developers to draw from, and I might also gain synergies by being able to use modules developed for the backend in the browser.
I’ve put the code from this article into a repo at ianfinch/golang-wasm-calc