loading...
Cover image for Let's write frontend in Go

Let's write frontend in Go

talentlessguy profile image v 1 r t l Updated on ・4 min read

UPD I wrote a CLI for automatic Go+WASM app setup and management, check it out.

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

First, you need Go 1.12+. You can install go1.12 from the website or with gvm. Second, you need the latest version of your browser to support WebAssembly. While writing this article, I use Mozilla 68.

Let's code!

Glue

Let's create glue.js and index.html to make WebAssembly work. Of course, WASM is magic that runs as a native platform, but it still needs some glue to load / display things.

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 (Go 1.11+) module:

go mod init

Install godom (wasm section):

go get -u -v github.com/siongui/godom/wasm

Go Code

Write some Go code:

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

Compile Go to WebAssembly

We need to compile all this stuff to make it work. To compile to wasm, we should set architecture to WASM, and OS to JS:

ARCH=WASM OS=js go build -o app.wasm index.go

Nice, we have a compiled file. It will be called "app.wasm".

Now try to open index.html file in a browser or run a local server using goexec:

goexec 'http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))'

We see hello world!

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

You can do a lot of funny things related to DOM thanks to WebAssembly. And Go is a good language for building web apps without JS. It is easy, fun and cool.

Please hit a "heart" button on this article

Follow me on

Twitter - v1rtl
Telegram - talentless_guy
GitHub - talentlessguy

Posted on by:

talentlessguy profile

v 1 r t l

@talentlessguy

I'm a teen web developer and a bit web designer. I like making interactive stuff with code and try to combine both tech & art.

Discussion

markdown guide
 

A major problem in webassembly+go is a bundle size. It is way larger in comparison to gopherjs due to runtime inclusion. TinyGo makes smaller binaries but it has different approach to external js calls (no syscall/js package). Anyway frontend in go sounds interesting when frameworks like vecty ("react in go") vugu ("vue in go") used.

 

Do you mean wasm binaries by bundle size?

Hm, I'll check TinyGo. Sounds interesting.

 

Following this tutorial, and I'm getting:


panic: syscall/js: Value.Call: property _makeFuncWrapper is not a function, got undefined wasm_exec.js:41:14
wasm_exec.js:41:14

Unfortunately, no help on github:
stackoverflow.com/questions/571545...

Any ideas?

 

what version of go are you using?

 

Thanks for the reply.
Yes, I've been using master wasm_exec.js instead one for my go version. Now it works like charm.

 

First time i actually see some real go assembly code, thx for sharing !

 

...It isn't Assembly. It is WebAssembly and these things differ by syntax and execution methods and purpose too. Assembly is used to compile code to machine level, WebAssembly is compiled to the lowlevel binaries that can be loaded with modules to the web

 

*my bad I meant web assembly, sorry !

 
 

Thank you, this is my first webassembly app with Go.
That's great! :)

 

you're welcome!

check this tool out, may be interesting: github.com/talentlessguy/go-web-app

 

Yes !
Thank you, now that I understand the principle, it's even better to have it automated and optimized.

I tested with:

  • Archlinux (5.2.3-arch1-1-ARCH):
  • tinygo with the tinygo_0.7.1_amd64.deb package and debtap + pacman
  • go get github.com/talentlessguy/go-web-app

I have a functional webassembly project that weighs only 46.4 kb

Wonderful :)

 
 

Just perfect! Looking forward to see future frontend libraries which will utilize WebAssembly

 

Thanks for sharing. Looks really interesting

 

This is the first time I've ever gotten excited about WebAssembly. Go + WebAssembly, now you're talkin my language :D

 

I'm now writing a VDOM in Go, dk if it can get faster than React's VDOM

 

Go now has a great WebAssembly support, even a special UMD script for better DX.

What does "DX" mean: Developer Experience?

 
 

I shared your article here t.me/theprogrammersclub and check out the group if you haven't already!

 

when I first read the title, it sounded like a blasphemy; but reading the content, I kinda liked it and I'm gonna try it haha