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