WebAssembly & TinyGo Basics
10 examples to get you started with WASM & TinyGo - 7 basic and 3 intermediate.
Prerequisites
- Install Go 1.26.x from go.dev/dl.
- Install a WASI runtime:
brew install wasmtime(macOS) or download from wasmtime.dev. - Optional: install TinyGo from tinygo.org/getting-started and verify with
tinygo version. - Create a scratch module:
mkdir wasmdemo && cd wasmdemo && go mod init example.com/wasmdemo.
Basic Examples
1. Compile hello world to WASI
The smallest proof that Go emits a runnable .wasm module.
package main
import "fmt"
func main() {
fmt.Println("hello from wasip1/wasm")
}GOOS=wasip1 GOARCH=wasm go build -o hello.wasm .
wasmtime run hello.wasmGOOS=wasip1selects WASI Preview 1 imports instead of Linux syscalls.- Output is a single
hello.wasmartifact - no separate Go binary. wasmtime runprovides the WASI host imports the module expects.
Related: Compiling with GOOS=wasip1 GOARCH=wasm - flags and linker options
2. Strip debug symbols for smaller WASM
Shrink download size with standard linker flags.
GOOS=wasip1 GOARCH=wasm go build -ldflags="-s -w" -o hello.wasm .-somits the symbol table;-womits DWARF debug info.- Safe for release builds; keep unstripped artifacts for profiling if needed.
- Size drops are modest compared to choosing TinyGo, but the flags are free.
Related: WASM Size Optimization: TinyGo vs gc WASM - deeper size tactics
3. Read WASI environment variables
WASI exposes os.Getenv through host imports.
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("USER=", os.Getenv("USER"))
}wasmtime run --env USER=ada hello-env.wasm- Not every host pre-populates the same keys as Linux.
- Pass explicit
--envflags in wasmtime for reproducible tests. - Treat environment as capability the host grants, not implicit global state.
4. Write a file through WASI pre-open
Filesystem access requires the host to pre-open directories.
GOOS=wasip1 GOARCH=wasm go build -o write.wasm .
mkdir -p /tmp/wasmout
wasmtime run --dir /tmp/wasmout::/data write.wasmos.WriteFile("/data/out.txt", []byte("ok"), 0o644)- Paths inside the guest map to host directories via
--dir host::guestsyntax. - Fails closed if the directory is not pre-opened - good for sandboxing.
- Mirror this pattern in wazero with
FSConfigdirectory mounts.
5. TinyGo hello on WASM
TinyGo emits far smaller modules when its stdlib subset suffices.
package main
import "fmt"
func main() {
fmt.Println("hello from tinygo wasm")
}tinygo build -target=wasm -o tinyhello.wasm .
wasmtime run tinyhello.wasm-target=wasmselects the generic WASM backend (verify board-specific targets separately).- Confirm which stdlib packages your TinyGo version supports before porting libraries.
- Expect faster cold starts on size-sensitive edge hosts.
Related: TinyGo for Microcontrollers & Constrained WASM - board targets
6. Browser build tag skeleton
Isolate browser-only entrypoints from WASI builds.
//go:build js && wasm
package main
import "syscall/js"
func main() {
js.Global().Get("console").Call("log", "hello browser")
select {}
}GOOS=js GOARCH=wasm go build -o main.wasm .select {}keepsmainalive so callbacks can fire.- Pair with
wasm_exec.jsfrom$(go env GOROOT)/misc/wasm/wasm_exec.jsin HTML hosts. - Do not import
syscall/jsinwasip1packages.
Related: syscall/js: Go in the Browser - DOM callbacks
7. List WASM exports with wasm-tools
Inspect the module surface before embedding.
wasm-tools print hello.wasm | head- Confirms
_start, memory export, and WASI import names. - Mismatched imports are the top reason instantiation fails in hosts.
- Add
wasm-toolsviacargo install wasm-toolsor your package manager.
Intermediate Examples
8. Embed WASI in Go with wazero
Run a guest module from a Go service without cgo.
package main
import (
"context"
"os"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)
func main() {
ctx := context.Background()
r := wazero.NewRuntime(ctx)
defer r.Close(ctx)
wasi_snapshot_preview1.MustInstantiate(ctx, r)
wasmBytes, _ := os.ReadFile("hello.wasm")
mod, _ := r.Instantiate(ctx, wasmBytes)
defer mod.Close(ctx)
start := mod.ExportedFunction("_start")
_, _ = start.Call(ctx)
}wasi_snapshot_preview1registers WASI imports the Go module needs.Instantiatecompiles and loads bytecode; reuseRuntimeacross requests in servers.- Pass
contextdeadlines to bound guest execution time.
Related: WASI Host Runtimes: wasmtime, wazero & Spin - runtime comparison
9. CI smoke test for WASM artifacts
Catch broken builds before deploy.
GOOS=wasip1 GOARCH=wasm go build -o /tmp/app.wasm ./...
wasmtime run /tmp/app.wasm- Add the job next to
GOOS=linux GOARCH=amd64compile checks. - Fail the pipeline on non-zero
wasmtime runexit codes. - Pin
wasmtimeandtinygoversions in CI images for reproducibility.
Related: WebAssembly & TinyGo Best Practices - CI and import hygiene
10. Compare binary sizes gc vs TinyGo
Make data-driven toolchain choices.
GOOS=wasip1 GOARCH=wasm go build -ldflags="-s -w" -o gc.wasm .
tinygo build -target=wasm -o tiny.wasm .
ls -lh gc.wasm tiny.wasm- Log sizes in ADRs when choosing TinyGo vs gc for edge functions.
- Pair size with benchmark data - TinyGo runtime differs from gc scheduler.
- Re-measure after Go minor upgrades; 1.26 improved heap memory, not always file size.
Related: Native Go vs TinyGo vs Rust WASM Decision Guide - ranked trade-offs
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).