How to make Go, WebAssembly and TypeScript work together
2023.11.29. Tags: Golang, Development history, Case
Recently in one of my pet projects I added Go code compiled in WebAssembly (WASM) for client-side execution. The interface is made with Svelte with TypeScript, and the logic is implemented in Golang. As a result, I got an interesting experience that I want to share: how to make Go, WebAssembly and TypeScript work together.
Table of contents
How to export Go function to JavaScript
Create a separate directory for Go module with go.mod
and main.go
files.
You cannot directly import functions from Go into JavaScript. Instead, your Go code running in a browser can create global functions or any other global variables for JavaScript.
You can register a global function for JavaScript like this:
package main
import "syscall/js"
func myFunc(this js.Value, args []js.Value) any {
return "hello"
}
func main() {
// create a global variable MyFunc in JavaScript
// and assign a function to it
js.Global().Set("MyFunc", js.FuncOf(myFunc))
// wait indefinitely so that Go does not terminate execution
// and the function remains available
<-make(chan struct{})
}
syscall/js
is a package from the Go standard library. The Go compiler and your IDE may complain that they don’t know it. To avoid this, you should add a directive to the beginning of main.go
that the file can be compiled only to WebAssembly:
//go:build wasm
// +build wasm
package main
import (
"syscall/js"
...
Compiling Go code into WebAssembly
Compile with environment variables GOOS=js
and GOARCH=wasm
. You can make a Makefile
for convenience:
build: copy-wasm-exec
GOOS=js GOARCH=wasm go build -o main.wasm
copy-wasm-exec:
cp $(shell go env GOROOT)/misc/wasm/wasm_exec.js .
The make build
command does the compilation. The make copy-wasm-exec
command copies the wasm_exec.js
library from your version of Go. We’ll need this library when running WebAssembly from JavaScript.
Running Go code in JavaScript
In order to execute Go code compiled into WebAssembly, you need to import wasm_exec.js
:
// $go is an alias for path to our Go module
import '$go/wasm_exec'
This file creates the Go
class. Its TypeScript type:
declare global {
export interface Window {
Go: {
new (): {
run: (inst: WebAssembly.Instance) => Promise<void>
importObject: WebAssembly.Imports
}
}
}
}
Running the WebAssembly binary in the browser is done using the WebAssembly.instantiateStreaming
, fetch
and the imported Go
class:
// `wasm` variable contains URL
// of our WASM binary
import wasm from '$go/main.wasm?url'
// load and run our Go code
export async function load() {
if (!WebAssembly) {
throw new Error('WebAssembly is not supported in your browser')
}
const go = new window.Go()
const result = await WebAssembly.instantiateStreaming(
// load the binary
fetch(wasm),
go.importObject
)
// run it
go.run(result.instance)
// wait until it creates the function we need
await until(() => window.MyFunc != undefined)
// return the function
return window.MyFunc
}
// helper Promise which waits until `f` is true
const until = (f: () => boolean): Promise<void> => {
return new Promise(resolve => {
const intervalCode = setInterval(() => {
if (f()) {
resolve()
clearInterval(intervalCode)
}
}, 10)
})
}
How to create a JavaScript class via WebAssembly
In JavaScript, a class is a function that creates an object. In order to create such a class from Go, we need to create functions for its constructor and methods. In each function we need to convert the types: at the beginning we convert js.Value
to the type we need and vice versa at the end.
type wrapper struct {
obj *mypkg.myObj
}
// Constructor of a JavaScript object
func NewObj(this js.Value, args []js.Value) any {
// Convert `js.Value` arguments to the types we need
name := args[0].String()
number := args[1].Int()
// Create an object
wrap := &wrapper{
obj: mypkg.NewObj(name, number)
}
// Convert our wrapper to
// JavaScript object with two methods
return js.ValueOf(map[string]any{
"set": js.FuncOf(wrap.Set),
"get": js.FuncOf(wrap.Get),
})
}
// The object's method
func (w *wrapper) Set(this js.Value, args []js.Value) any {
name := args[0].String()
number := args[1].Int()
res, err := w.obj.Set(name, number)
if err != nil {
return js.ValueOf(map[string]any{
"error": err.Error(),
})
}
return js.ValueOf(map[string]any{
"result": res,
})
}
...
Error handling
In Go code, you can’t throw a JavaScript exception. If you throw a panic in Go code, it will terminate and you will no longer be able to call its functions.
If your function can fail, you can return an object that contains either a result
or error
field. In TypeScript notation, the interface looks like this:
interface Result<T>{
result?: T
error?: string
}
Then error checking can be like this:
const { result, error } = myobj.get()
if (error != undefined) {
// handle error
} else {
// handle result
}
Create types for TypeScript
You should create TypeScript types for everything imported from Go:
declare global {
export interface Window {
NewObj: (name: string, num: number) => MyObj
}
}
export interface MyObj {
get() => Result<number>
set(name: string, num: number) => Result<number>
}
All together
You can see an example of how it all fits together on GitHub: