Go to browser. Building Web Applications with Web Assembly in Go


Functions and methods in golang

The most well known set of tools for compiling to wasm32 is emscripten, it can be used to compile an application written in C/C++ or any language that has a frontend compiler for LLVM. At the same time, the compiler replaces OpenGL and POSIX calls with the corresponding counterparts in the browser, which is used, for example, when compiling the skia library for the browser (canvaskit) from the C ++ source code, as well as porting existing libraries (for example, ffmpeg or opencv). But some programming languages ​​support wasm32 as one of the target platforms, among which are Kotlin (Native) and Go. In this article, we’ll discuss common questions about running Go applications in a browser environment and using the Vecty library to build web applications based on reusable components.

Compiling to the wasm32 target platform is supported in Go by setting the environment variables GOOS=js and GOARCH=wasm. Let’s start with the simplest program and gradually complicate the capabilities of our web application.

package main
 
import "fmt"
 
func main() {
    fmt.Println("Hello, WebAssembly World!")
}

Let’s compile the source code into wasm bytecode:

GOOS=js GOARCH=wasm go build -o hello.wasm

And then we will create a script to download and run the wasm file in HTML:

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <script src="https://habr.com/ru/company/otus/blog/666004/wasm_exec.js"></script>
        <script>
            if (WebAssembly) {
                 const go = new Go();
                 WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
                    go.run(result.instance);
                 });
            } else {
               console.log("WebAssembly is not supported in your browser")
            }
        </script>
    </head>
    <body></body>
</html>

The wasm_exec.js file can be obtained, for example, from here or from a Go installation (located in misc/wasm). It creates the necessary context for executing the Go application (for example, it creates implementations for syscall/js, which we will use to interact with the browser, and it also registers a Go prototype that will be used to bootstrap the bytecode and run the main function.

To bypass browser security restrictions (by default, loading network resources is prohibited if the page is opened via a local link in the file:// scheme), we will launch a docker container with nginx.

docker run --name gotest -d -p 80:80 -v `pwd`:/usr/share/nginx/html nginx

After calling localhost, you can see that the message is printed to the browser console. Now let’s connect to the DOM and try to call a Javascript function from our wasm application. To access JS, you need to connect the module syscall/js and use the global context to access registered symbols in js via the Global() call to access the window object. For example, to get the current address, you can use the following expression.

js.Global().Get("location").Get("href")

To call functions from Javascript or built into the browser, you can use the Call method from the object (or from the global context). For example, to display a notification dialog, you can use the call:

js.Global().Call("alert", "Hello, WASM")

To add an element to the DOM, you can combine calls using the document object:

document := js.Global().Get("document")
body := document.Call("querySelector", "body")
div := document.Call("createElement", "div")
div.Set("innerHTML", "Hello, WASM")
body.Call("appendChild", div)

Similarly, you can call JS functions from Go code. In the html file in

Reading from an empty pipe in the last line of main is necessary to avoid terminating the wasm code with all registered characters removed. Now, anywhere after calling the main function (launching the application via go.run), we can access the registered function and get the captcha image:

img = document.createElement("img");
img.src = captcha();
document.body.appendChild(img);

If we need to send a complex structure (for example, send an image and a voice captcha at the same time), then here we will encounter the problem that the structures are not sent directly to JS (there is a panic: ValueOf: invalid value error). One way to solve the problem can be serialization to JSON or other forms of packing structures into a single object, such as Protobuf).

This way we can use ready-made libraries developed for Go inside our browser. But creating websites in this way is extremely difficult and inefficient and tantamount to use. Let's turn our attention now to the libraries for creating dynamic web applications in Go.

Vecty Library

Vecty in many ways similar to React's reactive UI approach and offers a similar model of state-dependent components. Each component uses the vecty.Core structure and implements a Render method that renders a tree of elements using wrappers around HTML tags (in hexops/vecty/elem) as well as any other components created in the application. When defining a component, additional parameters can be specified that determine its state. An important difference from React is the need to notify state changes by calling the Render method (in this case, not the entire application can be rebuilt, but only part of the tree).

For example, a component could be defined like this:

type ScreenView struct {
	vecty.Core
	id int
}

func (p *ScreenView) Render() vecty.ComponentOrHTML {
	return elem.Div(
		vecty.Markup(
			vecty.Class("screen"+strconv.Itoa(p.id)),
			vecty.MarkupIf(powerOn && active == p.id, vecty.Class("selected"))),
		elem.Canvas(
			vecty.Markup(
				prop.ID("canvas"+strconv.Itoa(p.id)),
				vecty.Style("background", "black"),
				vecty.Property("width", strconv.Itoa(screenWidth)),
				vecty.Property("height", strconv.Itoa(screenHeight)),
			),
		),
	)
}

Here, the state is defined by the global variable powerOn and the internal identifier id. The following structures can be used in vecty.Markup:

  • prop.ID - change the value of the id property for the tag

  • prop.Name - change the value of the name property for the tag

  • vecty.Class - the value of the class attribute for the tag

  • vecty.MarkupIf - conditional insertion of values ​​(or tag), will be executed only if the first argument is true

  • vecty.Style - to override the tag's style attribute

  • vecty.Property - change an arbitrary field of the object associated with the HTML tag

  • vecty.Attribute - changing an arbitrary tag attribute

  • vecty.Data - change tag data-attributes

  • vecty.EventListener - for registering an event handler

  • vecty.UnsafeHTML - insert an arbitrary HTML fragment

In the future, the created components can be included in other components.

type Screens struct {
	vecty.Core
}

func (p *Screens) Render() vecty.ComponentOrHTML {
	return elem.Div(vecty.Markup(vecty.Class("centered")),
		elem.Div(
			vecty.Markup(vecty.Class("screens")),
			&LeftButton{},
			&ScreenView{id: 0},
			&ScreenView{id: 1},
			&ScreenView{id: 2},
			&ScreenView{id: 3},
			&RightButton{},
		),
	)
}

If you need to register an event handler, you can integrate the EventListener structure into vecty.Markup :

return elem.Data(vecty.Markup(
		vecty.Class("fa-button"),
		vecty.Class("right"),
		&vecty.EventListener{Name: "click", Listener: func(event *vecty.Event) {
      powerOn = !powerOn
      vecty.Rerender(emulator)
		}}), vecty.Text("\uF054"))

Importantly, updating the state can be done asynchronously within a goroutine, such as after a long network request is made, or complex and time-consuming algorithmic tasks in Go.

To create applications, a ready-made wrapper is available for using Material Design Components https://github.com/vecty-components/material.

Alternative solutions include Go App (implements a model of reusable components very similar to Vecty), Tango (uses an approach similar to AngularJS), Vugu (similar to the Vue.js model used).

An example application code using Vecty can be viewed on Github: https://github.com/AirCube-Project/emulator/

I also want to invite everyone to a free lesson on the topic: "Functions and methods in golang". Registration is available at this link.

BUT here you can watch the recording of the lesson "Structures of the golang language", which is taking place right now at the time of the publication of this material.