Leveraging Golang in NodeJS Applications: Part 2 (Wasm)

In a previous post, I demonstrated how to leverage Go’s CGO capabilities to interact with JavaScript runtimes. Now, I’ll show how to compile a Go library into WebAssembly format and integrate it into your JavaScript applications.

Choose the right Go compiler

Go’s ability to compile all dependencies into a single binary is convenient in most use cases, but it can be a limitation where the file size matters, such as microcontroller programming, web development, or malware engineering. Even though the default Go compiler optimises WebAssembly builds, if you plan to use your code in the browser, the community alternative TinyGo might be a better option.

TinyGo

You can skip this step, WebAssembly produced by default Go compiler works the same way. TinyGo can produce very compact WebAssembly code. See the installation instructions:

brew tap tinygo-org/tools
brew install tinygo

Creating a simple Go program

In order to interact with javascript golang provides syscall/js package, however before using it we need to specify appropriate go:build directive:

./main.go :

//go:build js && wasm
// +build js,wasm

package main

import (
  "fmt"
  "syscall/js"
)

func main() {}

We need to create a function which will be called from javascript. The function will accept js.Value as arguments and must return any interface:

func callback(this js.Value, args []js.Value) interface{} {
    var (
        arg1 = args[0].String()
        arg2 = args[1].Int()
    )

    fmt.Printlin(arg1, arg2)

    return nil
}

The first argument is a reference to this object, it results to the undefined if there’s no context. syscall/js provides number of methods to convert js types to Go and interact with javascript runtime.

We are interested in following features:

  • Ability to pass function arguments from JS to GO and vice-versa.
  • Ability to export functions from Go to JS.

Exporting Go functions

In order to convert Go function to JS callback we can use js.FuncOf:

js.FuncOf(callback) // js.Function

And then we can use method .Set("prop", js.Value) on a JS value. For example if we want to export our function globally:

js.Global().Set("global_callback", js.FuncOf(callback))

Full example:

//go:build js && wasm
// +build js,wasm

package main

import (
  "fmt"
  "syscall/js"
)

func callback(this js.Value, args []js.Value) interface{} {
    var (
        arg1 = args[0].String()
        arg2 = args[1].Int()
    )

    fmt.Printlin(arg1, arg2)

    return nil
}

func main() {
    wait := make(chan struct{}, 0)
    js.Global().Set("global_callback", js.FuncOf(callback))
    <-wait
}

This code will add a function in window - globalThis.global_callback.

Compiling to WASM

TinyGo:

GOOS=js GOARCH=wasm tinygo build -o mod.wasm main.go

Golang:

GOOS=js GOARCH=wasm go build -o mod.wasm main.go

Using our code in JS

First we need to install Go’s js runtime wasm_exec.js. This file maps all necessary API’s. Select one for your environment:

Golang wasm_exec.js TinyGo wasm_exec.js

Importing WASM:

Following code snippet works for Bun, Deno and ESM imports, the filename is mod.ts:

import "./wasm_exec.js";
// @ts-expect-error: no types
const go = new Go();

const code =
  await (await fetch(import.meta.url.replace("/mod.ts", "/mod.wasm")))
    .arrayBuffer();

const wasmMmodule = await WebAssembly.instantiate(code, go.importObject);
const wasm = wasmMmodule.instance;

go.run(wasm);

// call global function
global_callback('Hello World!', 123)

A polyfilled example for importing wasm can be found here.

Passing data from JS to Go

We’ve successfully created and called a global function. However, poluting globals is not a best practice if we have a big library. A better way is to have a function for our go library, instead of puting our funtions to global scope, we will create a local javascript object for this purpose:

import "./wasm_exec.js";
// @ts-expect-error: no types
const go = new Go();

const code =
  await (await fetch(import.meta.url.replace("/mod.ts", "/mod.wasm")))
    .arrayBuffer();

const wasmMmodule = await WebAssembly.instantiate(code, go.importObject);
const wasm = wasmMmodule.instance;

go.run(wasm);

const LibraryExports = InitMyLibrary();


export default LibraryExports;

We’ve created LibraryExports object and passed it to the init function InitMyLibrary then exported LibraryExports.

Global init function:

package main

import (
  "fmt"
  "syscall/js"
)

func callback(this js.Value, args []js.Value) interface{} {
    var (
        arg1 = args[0].String()
        arg2 = args[1].Int()
    )

    fmt.Printlin(arg1, arg2)

    return nil
}

func InitStatExports(this js.Value, args []js.Value) interface{} {
  exports := js.Global().Get("Object").New()
  exports.Set("callback", js.FuncOf(callback))
  return exports
}

func main() {
    wait := make(chan struct{}, 0)
    js.Global().Set("InitStatExports", js.FuncOf(InitStatExports))
    <-wait
}

And now after compilation, we can use our library in JS as follows:

import MyLib from './mod.ts'

MyLib.callback('Hello World', 123)

/* Hello World 123 */

This approach should get you started, but there’s one more thing to talk about - javascript arrays.

Receiving JS arrays in Go

js.Value gives us the methods to manipulate data in javascript objects in place (get,set,etc). However, if we want to use Go data structures, we need to create new go structures. For example, with arrays you’ll need to write utility functions like following:

func JSFloatArray(arg js.Value) []float64 {
  arr := make([]float64, arg.Length())
  for i := 0; i < len(arr); i++ {
    arr[i] = arg.Index(i).Float()
  }
  return arr
}

And if you need to write resut into a js object or array you need to pass an instance to Go callback. There’s no workaround at the time, and may be it is for the best, but watch for mem leacks.

Full example

./main.go:

//go:build js && wasm
// +build js,wasm

package main

import (
  "fmt"
  "syscall/js"
)

func callback(this js.Value, args []js.Value) interface{} {
    var (
        arg1 = args[0].String()
        arg2 = args[1].Int()
    )

    fmt.Printlin(arg1, arg2)

    return nil
}

func InitStatExports(this js.Value, args []js.Value) interface{} {
  exports := js.Global().Get("Object").New()
  exports.Set("callback", js.FuncOf(callback))
  return exports
}

func main() {
    wait := make(chan struct{}, 0)
    js.Global().Set("InitStatExports", js.FuncOf(InitStatExports))
    <-wait
}

./mod.ts:

import "./wasm_exec.js";

// @ts-expect-error: no types
const go = new Go();

const code =
  await (await fetch(import.meta.url.replace("/mod.ts", "/mod.wasm")))
    .arrayBuffer();

const wasmMmodule = await WebAssembly.instantiate(code, go.importObject);
const wasm = wasmMmodule.instance;

go.run(wasm);

const LibraryExports = {};

InitMyLibrary(LibraryExports);

export default LibraryExports;
dev:
    GOOS=js GOARCH=wasm tinygo build -o mod.wasm main.go

prod:
    GOOS=js GOARCH=wasm tinygo build -o mod.wasm -no-debug main.go

Real world example

See example the of using gonb/stats in javascript

Peace ✌️