Thanks to WebAssembly, you can write Frontend on Go

  • Tutorial
Original article.

In February 2017, a member of the go team Brad Fitzpatrick offered to make support for WebAssembly in the language. Four months later, in November 2017, the author GopherJS Richard Muziol began to implement the idea. And, finally, the full implementation was in the master. Developers will receive wasm around August 2018, with version 1.11 go . As a result, the standard library takes on almost all the technical difficulties with importing and exporting functions familiar to you, if you have already tried compiling C in wasm. It sounds promising. Let's see what can be done with the first version.



All examples in this article can be run from docker containers that are in the author's repository :

docker container run -dP nlepage/golang_wasm:examples
# Find out which host port is used
docker container ls

Then go to localhost : 32XXX /, and go from one link to another.

Hi, wasm!


Creating a basic “hello world” and concepts is already fairly well documented (even in Russian ), so let's just move on to more subtle things more quickly.

The most necessary is a freshly compiled version of Go that supports wasm. I will not describe the installation step by step , just know what is needed already in the master.

If you don’t want to worry about it, Dockerfile c go is available in the golub-wasm repository on github , or even faster you can take an image from nlepage / golang_wasm .

Now you can write the traditional one helloworld.goand compile it with the following command:

GOOS=js GOARCH=wasm go build -o test.wasm helioworld.go

The environment variables GOOS and GOARCH are already set in the nlepage / golang_wasm image, so you can use a file Dockerfilelike this to compile:

FROM nlepage/golang_wasm
COPY helloworld.go /go/src/hello/
RUN go build -o test.wasm hello

The final step is to use the files wasm_exec.htmland those wasm_exec.jsavailable in the go repository каталоге misc/wasmor in the docker nlepage / golang_wasm image in the directory /usr/local/go/misc/wasm/for execution test.wasmin the browser (wasm_exec.js expects a binary file test.wasm, so use this name).
You just need to give 3 static files using nginx, for example, then wasm_exec.html will display the “run” button (it will turn on only if it is test.wasmloaded correctly).

It is noteworthy that test.wasmit is necessary to serve with the MIME type application/wasm, otherwise the browser will refuse to execute it. (for example, nginx needs an updated mime.types file ).

You can use the nginx image from nlepage / golang_wasmwhich already includes the corrected MIME type, wasm_exec.htmland wasm_exec.jsin the code> / usr / share / nginx / html / directory.

Now click the “run” button, then open the console of your browser, and you will see the console.log greeting (“Hello Wasm!”).


A full example is available here .

Call JS from Go


Now that we have successfully launched the first WebAssembly binary file compiled from Go, let's take a closer look at the possibilities provided.

The new syscall / js package is included in the standard library, consider the main file - js.go.
A new type js.Valueis available that represents the JavaScript value.

It offers a simple API for managing JavaScript variables:

  • js.Value.Get()and js.Value.Set()return and set the values ​​of the object's fields.
  • js.Value.Index()and js.Value.SetIndex()access the object by index for read and write.
  • js.Value.Call() calls the object's method as a function.
  • js.Value.Invoke() calls the object itself as a function.
  • js.Value.New() calls the new operator and uses its own knowledge as a constructor.
  • A few more methods for getting the javascript value in the appropriate type go, for example js.Value.Int()or js.Value.Bool().

And additional interesting methods:

  • js.Undefined()will give js.Value appropriate undefined.
  • js.Null()will give the js.Valueappropriate null.
  • js.Global()returns js.Value, giving access to the global scope.
  • js.ValueOf() accepts primitive go types and returns the correct js.Value

Instead of displaying a message in os.StdOut, let's display it in the alert window using window.alert().

Since we are in the browser, the global scope is a window, so you first need to get alert () from the global scope:

alert := js.Global().Get("alert")

Now we have a variable alert, in the form js.Value, which is a reference to window.alertJS, and we can use the function call through js.Value.Invoke():

alert.Invoke("Hello wasm!")

As you can see, there is no need to call js.ValueOf () before passing Invoke arguments, it takes an arbitrary number interface{}and passes values ​​through ValueOf itself.

Now our new program should look like this:

package main
import (
    "syscall/js"
)
funcmain() {
    alert := js.Global().Get("alert")
    alert.Invoke("Hello Wasm!")
}

As in the first example, you just need to create a file with the name test.wasm, and leave wasm_exec.htmlit wasm_exec.jsas it was.
Now, when we press the “Run” button, an alert window appears with our message.

A working example is in the folder examples/js-call.

Call Go from JS.


Calling JS from Go is quite simple, let's take a closer look at the package syscall/js, the second file to view is callback.go.

  • js.Callback type wrapper for Go function, for use in JS.
  • js.NewCallback()a function that accepts a function (the receiving slice js.Valueand not returning anything), and returns js.Callback.
  • Some mechanics to manage active callbacks and js.Callback.Release()that should be called to destroy the callback.
  • js.NewEventCallback()similarly js.NewCallback(), but the wrapped function takes only 1 argument - the event.

Let's try to do something simple: run Go fmt.Println()by JS.

We'll make some changes to wasm_exec.htmlbe able to get a callback from Go to call it.

asyncfunctionrun() {
    console.clear();
    await go.run(inst);
    inst = await WebAssembly.instantiate(mod, go.ImportObject); // сброс экземпляра
}

This launches the wasm binary and waits for it to complete, then reinitializes it for the next run.

Let's add a new function that will receive and save the Go callback and change the state Promiseupon completion:

let printMessage // Our reference to the Go callbacklet printMessageReceived // Our promiselet resolvePrintMessageReceived // Our promise resolver functionsetPrintMessage(callback) { 
    printMessage = callback 
    resolvePrintMessageReceived()
}

Now let's adapt the function run()to use callback:

asyncfunctionrun() {
    console.clear()
    // Create the Promise and store its resolve function 
    printMessageReceived = newPromise(resolve => { 
        resolvePrintMessageReceived = resolve
    })
    const run = go.run(inst) // Start the wasm binaryawait printMessageReceived // Wait for the callback reception 
    printMessage('Hello Wasm!') // Invoke the callback await run // Wait for the binary to terminate
    inst = await WebAssembly.instantiate(mod, go.importObject) // reset instance
}

And this is on the JS side!

Now in the Go part you need to create a callback, send it to the JS side and wait for the function to be needed.

var done = make(chanstruct{})

Then must write a real function printMessage():

funcprintMessage(args []js.Value) {
    message := args[0].Strlng()
    fmt.Println(message)
    done <- struct{}{} // Notify printMessage has been called
}

The arguments are passed through the slice []js.Value, so you need to call js.Value.String()in the first slice element to get the message in the Go line.
Now we can wrap this function in a callback:

callback := js.NewCallback(printMessage)
defer callback.Release() // to defer the callback releasing is a good practice   

Then call the JS function setPrintMessage(), just like when calling window.alert():

setPrintMessage := js.Global.Get("setPrintMessage")
setPrintMessage.Invoke(callback)

The last thing to do is wait for the callback call in main:

<-done

This last part is important because the callbacks are executed in a dedicated goroutine, and the main goroutine must wait for a callback, otherwise the wasm binary will be stopped prematurely.

The resulting Go program should look like this:

package main
import (
    "fmt""syscall/js"
)
var done = make(chanstruct{})
funcmain() {
    callback := js.NewCallback(prtntMessage)
    defer callback.Release()
    setPrintMessage := js.Global().Get("setPrintMessage")
    setPrIntMessage.Invoke(callback)
    <-done
}
funcprintMessage(args []js.Value) {
    message := args[0].Strlng()
    fmt.PrintIn(message)
    done <- struct{}{}
}

As in the previous examples, create a file with the name test.wasm. You also need to replace wasm_exec.htmlour version, and wasm_exec.jswe can reuse it.

Now, when you press the “run” button, as in our first example, the message is printed in the browser console, but this time it is much better! (And more difficult.) The

working example in the baker docker file is available in the folder examples/go-call.

Long job


The Go from JS call is a bit more cumbersome than the Go JS call, especially on the JS side.

This is mainly due to the fact that you need to wait for the result of the Go callback to be sent to the JS side.

Let's try something else: why not organize a wasm binary file that will not be completed immediately after a callback call, but will continue to work and receive other calls.
This time let's start from Go, and like in our previous example, we need to create a callback and send it to the JS side.

Add a call counter to track how many times the function has been called.

Our new feature printMessage()will print the received message and the value of the counter:

var no intfuncprintMessage(args []js.Value) { 
    message := args[0].String() 
    no++
    fmt.Printf("Message no %d: %s\n", no, message)
}

Creating a callback and sending it to the JS side is the same as in the previous example:

callback := js.NewCallback(printMessage)
defer callback.Release()
setPrintMessage := js.Global().Get("setPrintMessage")
setPrIntMessage.Invoke(callback)

But this time we have no channel doneto notify us of the termination of the main gorutin. One way could be to permanently block the main goroutin empty select{}:

select{}

This is not satisfactory, our binary wasm will just hang in memory until the browser tab closes.

You can listen to the event beforeunloadon the page, you will need a second callback to receive the event and notify the main gorutina through the channel:

var beforeUnloadCh = make(chanstruct{})

This time, the new function beforeUnload()will only accept the event as a single js.Valueargument:

funcbeforeUnload(event js.Value) {
    beforeUnloadCh <- struct{}{}
}

Then we wrap it up in a callback using js.NewEventCallback()and register it on the JS side:

beforeUnloadCb := js.NewEventCallback(0, beforeUnload)
defer beforeUnloadCb.Release()
addEventLtstener := js.Global().Get("addEventListener")
addEventListener.Invoke("beforeunload", beforeUnloadCb)

Finally, replace the empty blocking selectfor reading from the channel beforeUnloadCh:

<-beforeUnloadCh
fmt.Prtntln("Bye Wasm!")

The final program looks like this:

package main
import (
    "fmt""syscall/js"
)
var (
    no             int
    beforeUnloadCh = make(chanstruct{})
)
funcmain() {
    callback := js.NewCallback(printMessage)
    defer callback.Release()
    setPrintMessage := js.Global().Get("setPrintMessage")
    setPrIntMessage.Invoke(callback)
    beforeUnloadCb := js.NewEventCallback(0, beforeUnload)
    defer beforeUnloadCb.Release()
    addEventLtstener := js.Global().Get("addEventListener")
    addEventListener.Invoke("beforeunload", beforeUnloadCb)
    <-beforeUnloadCh
    fmt.Prtntln("Bye Wasm!")
}
funcprintMessage(args []js.Value) {
    message := args[0].String()
    no++
    fmt.Prtntf("Message no %d: %s\n", no, message)
}
funcbeforeUnload(event js.Value) {
    beforeUnloadCh <- struct{}{}
}

Previously, on the JS side, loading the wasm binary file looked like this:

const go = new Go()
let mod, inst
WebAssembly
    .instantiateStreaming(fetch("test.wasm"), go.importObject)
    .then((result) => {
        mod = result.module
        inst = result.Instance
        document.getElementById("runButton").disabled = false
    })

Let's adapt it to run a binary file immediately after downloading:

(asyncfunction() {
    const go = new Go()
    const { instance } = await WebAssembly.instantiateStreaming(
        fetch("test.wasm"),
        go.importObject
    )
    go.run(instance)
})()

And replace the “Run” button with a message field and a button to call printMessage():

<inputid="messageInput"type="text"value="Hello Wasm!"><buttononClick="printMessage(document.querySelector('#messagelnput').value);"id="prtntMessageButton"disabled>
    Print message
</button>

Finally, a function setPrintMessage()that accepts and stores a callback should be simpler:

let printMessage;
functionsetPrintMessage(callback) {
    printMessage = callback;
    document.querySelector('#printMessageButton').disabled = false;
}

Now, when we press the “Print message” button, we should see a message of our choice and a call counter printed in the browser console.
If we check the box “Preserve log” of the browser console and refresh the page, we will see the message “Bye Wasm!”.



Sources are available in a folder examples/long-runningon github.

So what is next?


As you can see, the learned syscall/jsAPI does its job and allows you to write complex things with a small amount of code. You can write to the author , if you know the easier way.
At the moment, it is not possible to return a value to JS directly from the Go callback.
It should be borne in mind that all callbacks are executed in the same goroutin, so if you do some blocking operations in the callback, do not forget to create a new goroutin, otherwise you will block all other callbacks.
All the basic functions of the language are already available, including concurrency. For now, all goroutins will work in the same thread, but this will change in the future .
In our examples, we used only the fmt package from the standard library, but everything is available that is not trying to escape from the sandbox.

It seems that the file system is supported through Node.js.

Finally, how about performance? It would be interesting to run some tests to see how Go wasm compares with equivalent pure JS code. Someone hajimehoshi made measurements, how different environments work with integers, but the technique is not very clear.



Do not forget that Go 1.11 has not even been officially released yet. In my opinion, very good for experimental technology. Those who are interested in performance tests may torment their browser .
The main niche, as the author notes, is the transfer of already existing go code from the server to the client. But with new standards, you can do completely offline applications , and the wasm code is saved in compiled form. It is possible to transfer many utilities to web, agree, it is convenient?

Also popular now: