Go and TinyGo

19.1 Standard Go

Go binaries are large (2+ MB) because they include the runtime and GC. Go 1.24 added //go:wasmexport to export functions for WASI reactors.

// main.go: browser target (GOOS=js GOARCH=wasm)
//go:build js && wasm
package main

import (
	"fmt"
	"syscall/js"
)

func add(this js.Value, args []js.Value) any {
	return args[0].Int() + args[1].Int()
}

func main() {
	fmt.Println("Go Wasm loaded")
	js.Global().Set("goAdd", js.FuncOf(add))
	doc := js.Global().Get("document")
	p := doc.Call("createElement", "p")
	p.Set("textContent", "Hello from Go")
	doc.Get("body").Call("appendChild", p)
	select {} // keep the program running
}
GOOS=js GOARCH=wasm go build -o main.wasm
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" .     # glue (misc/wasm in older Go)
GOOS=wasip1 GOARCH=wasm go build -o app.wasm       # WASI preview 1 target (Go 1.21+)
wasmtime app.wasm
<script src="wasm_exec.js"></script>
<script>
  const go = new Go();
  WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then(r => go.run(r.instance));
</script>

19.2 TinyGo

TinyGo produces much smaller binaries (tens of KB), is popular for plugins (Envoy proxy-wasm, Extism, Spin), and supports a subset of the stdlib and reflection.

tinygo build -o small.wasm -target wasm ./main.go      # browser (much smaller output)
tinygo build -o app.wasm -target wasip1 ./main.go
tinygo build -o comp.wasm -target wasip2 ./main.go     # components