# Intro
Recently, Go 1.12 released. Go now has a great WebAssembly support, even a special UMD script for better DX.
So, if we have WebAssembly, it means that we can access DOM.
Go even has a special package for that – syscall/js
.
It basically just gets / sets global objects or calls functions.
So, to avoid writing ugly Call()
& Get("document")
code, we’ll be using godom, a library with predefined shortened functions.
It is mostly used for GopherJS (Go to JS compiler), but has WebAssembly support as well.
# Requirements
- go 1.12+
- Firefox 68 (was used during writing the article) / Chrome 69 / any other browser that supports WebAssembly
# Let’s code!
# Glue
Let’s create glue.js and index.html to make WebAssembly work. Even though WASM is magic that runs natively it still needs some glue to load itself.
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Go frontend app</title>
<script src="wasm_exec.js"></script>
<script src="glue.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
Here we attached two scripts and added a target container.
Don’t forget to copy wasm_exec
script!
cp $(go env GOROOT)/misc/wasm/wasm_exec.js
glue.js
const go = new Go()
WebAssembly.instantiateStreaming(
fetch('app.wasm', {
headers: {
'Content-Type': 'application/wasm',
},
}),
go.importObject,
).then((result) => go.run(result.instance))
In our glue code, we created an instance of a special Go class for better error messages and some setup especially for Go, launched a WebAssembly stream using fetch (e.g. wasm module loads asynchronously), created a new WebAssembly instance, and ran it.
Our frontend setup is done. Let’s do some stuff with Go.
# Go Setup
Init go module:
go mod init
Install godom (wasm package):
go get -u -v github.com/siongui/godom/wasm
# Go Code
At first let’s write a simple Hello World markup:
package main
import (
dom "github.com/siongui/godom/wasm"
)
func main() {
app := dom.Document.GetElementById("app")
app.SetInnerHTML(`
<div>
<h1>Hello World</h1>
<p>This page is built with Go, GoDOM and WebAssembly</p>
</div>
`)
}
As you can see here, we selected our #app
div defined in an HTML file.
Then we modify a property called “InnerHTML”.
godom doesn’t have a function for every DOM interaction but you can still use syscall/js
“Call” for such cases.
# Compile Go to WebAssembly
We need to compile our go code to wasm to make it work. To compile to wasm, we should set system architecture to WASM, and OS to JS. Those is certainly no real OS called “JS” or processor architecture “WASM”. Go uses them to detect that we want to compile it to WebAssembly instead of building a native executable.
In the terminal run this command:
ARCH=WASM OS=js go build -o app.wasm index.go
Now we have a compiled file. It is called “app.wasm”.
The last step is running a server to see the result. There are a few quick options that you can use:
- Using Go goexec package:
goexec 'http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))'
- Using Python:
python -m http.server -p 8080
- Using Node.js and serve module:
serve -p 8080
We see hello world!
Hello World
This page is built with Go, GoDOM and WebAssembly
# Interactive DOM
It is already very fun to write webpages with innerHTML
but let’s make some more complex stuff. We will attach a handler to a button and also add content with JS.
First, we import syscall/js
because godom doesn’t have some functions we need:
package main
import (
dom "github.com/siongui/godom/wasm"
"syscall/js"
)
Then we take our container (#app
), create two elements – <span>
and <button>
.
app := dom.Document.GetElementById("app")
button := dom.Document.CreateElement("button")
text := dom.Document.CreateElement("span")
Fine, let’s add some text to our button.
// Set button text
button.Set("textContent", "Click on me")
Now here all the magic happens. We’re gonna make a callback handler for click event. syscall/js
has a special wrapper for JavaScript functions js.FuncOf
and Func
is an interface for FuncOf
.
// Callback for click event
var cb js.Func
cb = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
text.Set("textContent", "Button was clicked")
return nil
})
// Add event listener to a button
button.Call("addEventListener", "click", cb)
We use an array for JS arguments because in JS we pass them as a simple list of things for functions:
// Small example
const args = (...arguments) => console.log(arguments)
args(0, 1, 2)
The last thing we need to do is to append our elements to container:
app.Call("appendChild", text)
app.Call("appendChild", button)
But there is one small problem…
wasm_exec.js:378 Uncaught Error: bad callback: Go program has already exited
at global.Go._resolveCallbackPromise (wasm_exec.js:378)
at wasm_exec.js:394
at <anonymous>:1:1
Go program is already finished and it skips our callbacks. Let’s fix it.
We don’t want Go to exit our program so we have to use channels. We need to put this in the beginning of our main function:
// Create a channel that doesn't let our Go program exit
c := make(chan bool)
And in the end we put this:
<-c
Now our Go program doesn’t exit even with callbacks!
Let’s try, it works!
# Conclusion
Although, all the API calls for now are made through JS, the fact that we can now pick other language than JavaScript is impressive. Probably, in the future, WebAssembly will get it’s own DOM API.