Skip to content

Embed Wago in Go

We will build a Go program that loads fib.wasm and prints the thirtieth Fibonacci number. The same runtime shape works inside a server, worker, or desktop application.

1. Create a Go module

sh
mkdir wago-embed
cd wago-embed
go mod init example.com/wago-embed
go get github.com/wago-org/wago

Wago is a Go library, so it does not compile Rust, TinyGo, AssemblyScript, or C source. Compile guest source with its own toolchain and give Wago the resulting .wasm file.

For this tutorial, download a ready-made module:

sh
curl -fsSL \
  https://raw.githubusercontent.com/wago-org/wago/main/tests/testdata/fib.wasm \
  -o fib.wasm

2. Create the host program

Create main.go:

go
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/wago-org/wago"
)

func main() {
	wasm, err := os.ReadFile("fib.wasm")
	if err != nil {
		panic(err)
	}

	rt := wago.NewRuntime()
	defer rt.Close()

	mod, err := rt.Compile(wasm)
	if err != nil {
		panic(err)
	}

	ctx := context.Background()
	inst, err := rt.Instantiate(ctx, mod)
	if err != nil {
		panic(err)
	}
	defer inst.Close()

	out, err := inst.Call(ctx, "fib", wago.ValueI32(30))
	if err != nil {
		panic(err)
	}

	fmt.Println(out[0].I32())
}

3. Run it

sh
go run .

You should see:

text
832040

The program did four pieces of work:

  1. NewRuntime created the application-level runtime.
  2. Compile decoded, validated, and compiled the module.
  3. Instantiate created one isolated set of guest state.
  4. Call checked the argument against the export signature and invoked fib.

A missing export, wrong value type, guest trap, or cancelled context comes back as an error.

4. Inspect the boundary

Add these lines after Compile:

go
fmt.Println("exports:", mod.Exports())
fmt.Println("imports:", mod.Imports())
fmt.Println("capabilities:", mod.RequiredCapabilities())

Run the program again. fib.wasm reports an export and no host requirements. In an application that accepts third-party modules, inspect this boundary before instantiation and decide what the guest may access.

5. Reuse compiled code

The useful lifetime usually looks like this:

text
process or service
└── Runtime
    ├── compiled Module
    ├── Instance for request A
    └── Instance for request B

Create one runtime for a meaningful application boundary. Compile a module once, then instantiate it more than once when callers need isolated memories, tables, or globals.

Always close each instance. A direct instance remains caller-owned even when the runtime owns the compiler and plugin resources around it.

Keep compiled input alive

After a successful Compile, do not modify the input byte slice while the compiled module is alive. Wago may retain views into that storage to avoid copying the whole module.

6. Put a deadline on guest work

Use the request or job context you already have:

go
ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
defer cancel()

out, err := inst.Call(ctx, "work", wago.ValueI64(1))

Add time to the import block when you try this. Cancellation interrupts running Wasm and returns an error. Close the instance according to your application's lifecycle instead of assuming cancellation disposed it.

When the low-level API fits better

The package-level Compile, Instantiate, and Invoke functions use raw 64-bit call slots:

go
compiled, err := wago.Compile(nil, wasm)
inst, err := wago.Instantiate(compiled, wago.InstantiateOptions{})
out, err := inst.Invoke("add", wago.I32(2), wago.I32(40))
fmt.Println(wago.AsI32(out[0]))

Use this for a small embedding that does not need runtime plugins or lifecycle hooks. Start with Runtime and typed Value calls for applications expected to grow.

Continue with Host functions and guest memory when the module needs to call back into Go. The repository also has a runnable typed-runtime example.

Released under the Apache 2.0 License.