Information


Blog Posts


Collections



Contact


Things Ian Says

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:

Basic Web Page

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:

Styled Web Page

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:

The Loading Screen

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:

Web Server

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:

document.getElementById(“loading”)

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:

document.getElementById(“loading”).style.setProperty(“display”, “none”)

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:

  1. Get my .wasm file from the web server
  2. Create a runnable instance of that file
  3. 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:

Form Activation

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:

Simple Run

I can also change one of the values to not be valid (for example X), to test my error display:

Simple Run Error

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:

Program 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:

Final Version

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:

Out of Memory

Digging around a bit more, I found that my .wasm file was 1.4M:

Large WASM file

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:

Final Version

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