Creating Web Components in Go + wasm

I recently saw on Twitter about how Golang now supports WebAssembly. I've become a bit of a fan of Go so I was pretty excitted to try it out for myself. For me printing Hello world to the console isn't enough so I thought I'd try building a <hello-world> web component instead. Luckily the interop story with JavaScript is pretty solid (with caveats, explained later) so it wasn't much work at all.

Go doesn't yet support wasm in any of its releases, so you'll have to build from source. This article walks through building it, but you don't need to build from that branch any more, go.googlesource.com's master branch has what you need.

With everything set up, here's the Go code to create a web component, main.go:

package main

import (
  "syscall/js"
)

func main() {
  c := make(chan struct{}, 0)

  init := js.NewCallback(func(i []js.Value) {
    element := i[0]
    element.Set("innerHTML", "Hello world!")
  })

  js.Global.Call("makeComponent", "hello-world", init)

  <-c
}

There are a couple of interesting things going on here. First, the channel is what keeps the code running. Otherwise the Go runtime would garbage collect the program.

Secondly, js.Global.Call does exactly what you expect. We are calling a global function called makeComponent.

Currently JavaScript can't call into Go code until it has been first given a function to call (this is what NewCallback provides). So Go has to initiate communication, but once it does so you can freely communicate back and forth. This does mean, however, that you must provide it a global to call. No big deal.

The makeComponent function is what actually creates a custom element class, it's defined in my HTML like so:


<script>
  makeComponent = function(name, init) {
    const Element = class extends HTMLElement {
      constructor() {
        super();
        init(this);
      }
    };

    customElements.define(name, Element);
  };
</script>

Pretty simple, this defines a new custom element and calls the init function when an element is constructed. This is the function that Go provided with js.NewCallback

To build you have to change the GOROOT, GOARCH, and GOOS flags. My Makefile looks like:

public/example.wasm: main.go
        GOROOT=~/gowasm GOARCH=wasm GOOS=js ~/gowasm/bin/go build -o public/example.wasm main.go

clean:
        rm public/example.wasm
        .PHONY: clean

Impression

And that's it! It's quite simple. I was pretty impressed with the interop between Go and JavaScript, that's not something I've seen does as nicely from the other wasm languages I've checked out (admittedly I haven't done a lot of looking). There is an open issue to make it possible to export functions from your Go program that would be callable from JavaScript, making the communication be able to be initiated from either side.

Even as it stands now you could easily build nicer APIs on top of what is provided.

Caveats

There are a couple of problems I noticed in my experimentation:

  1. After a while developing I would mysteriously start getting out of memory errors that wouldn't go away no matter what I changed in my code. Creating a new tab would fix it. This makes me suspect that perhaps this is a Chromium bug and not Go, however. Still something to look out for.
  2. The resulting .wasm file is 1.3mb. For me this takes it out of the running for most things that i build. I'm not sure what the reason is for this size or if it's something they can fix in the future. It might be a matter of optimizations, or it might be that a garbage collector is included in this size.