WebAssembly with Go: Step-by-Step Guide for Browser & Edge

This guide is for Apple Silicon (arm64) Mac and covers everything from setup to running.
We'll build and run Go + WASM examples targeting both browser (GOOS=js) and WASI/edge (GOOS=wasip1).
You'll also find explanations of WASM / WASI concepts, differences between Wasmtime vs WasmEdge.
π§ What is WASM? Why is it Important?β
WebAssembly (WASM) is a format and runtime that compiles code from languages (C/C++/Rust/Go, etc.) into a small, fast, and secure (sandboxed) bytecode.
Initially designed for browsers, it now runs in non-browser environments like edge, server, and IoT.
Why is it popular?
- Portability: Compile once β the same
.wasmfile runs on different platforms and CPU architectures. - Security: Sandbox ensures no default access to files/network (no access unless explicitly granted).
- Performance: Faster than JS; near-native speed (depending on workload).
- Fast startup: Millisecond-level βcold startβ - ideal for serverless/edge.
- Small size & deterministic behavior: Easy to deploy and isolate in production.
π§© What is WASI? Its Relation to WASMβ
WASI (WebAssembly System Interface) provides standard system capabilities (arguments, stdout/err, time/clock, file access - if permitted) to WASM modules outside the browser.
Itβs the key standard that brings WASM to edge and server environments.
- WASM: Secure bytecode + virtual machine.
- WASI: System interface to make this bytecode behave like an βapplication.β
Why is it needed?
Outside the browser, tasks like reading files, accessing arguments, writing to stdout, or measuring time are not possible without WASI (or rely on vendor-specific APIs).
π§ͺ Why Go + WASM?β
- Go offers productivity with a ready-to-use standard toolchain.
- GOOS=js enables browser-targeted JSβGo interaction via the
syscall/jsbridge. - GOOS=wasip1 produces WASI-compatible CLI/agents for edge/server.
- TinyGo generates much smaller
.wasmfiles when needed.
π Building WASM Applications with Goβ
Now that we understand the concepts, let's build practical examples. We'll create a simple statistical calculation demo app (mean and standard deviation) that demonstrates the same functionality implemented in two different ways:
- Browser target (
GOOS=js): A mean calculation function callable from JavaScript via thesyscall/jsbridge. - WASI/Edge target (
GOOS=wasip1): A CLI application that calculates mean, standard deviation, and z-scores from command-line arguments.
By implementing the same core logic (statistical calculations) in both environments, you'll clearly see how the same Go code adapts differently for browser vs. edge/server contexts.
π§° Setup (Apple Silicon - arm64)β
# Install Go (recommended via Homebrew)
brew install go
echo 'export PATH="/opt/homebrew/bin:$PATH"' >> ~/.zshrc
exec zsh
go version # Should show go1.21+ darwin/arm64 (or later)
# Install WASM runtimes (for WASI) - one of these is enough
brew install wasmtime
wasmtime --version
brew install wasmedge
wasmedge --version
π Project File Structureβ
wasm/
βββ browser/ # Browser target (GOOS=js)
β βββ go.mod # Go module file (browser module)
β βββ main.go # Go code (JS β Go interaction)
β βββ main.wasm # Compiled WebAssembly file
β βββ wasm_exec.js # Go runtime bridge file
β βββ index.html # Browser interface (loads WASM)
β
βββ wasi/ # WASI / Edge target (GOOS=wasip1)
βββ go.mod # Go module file (wasi-demo module)
βββ main.go # Go code (CLI / Statistical Calculation)
βββ stats.wasm # Compiled WASM (runs in Wasmtime / WasmEdge)
π‘ Tip:
- Each folder should be an independent module with its own
go mod init(browser and wasi separate).main.wasmandwasm_exec.jsmust be in the same directory forindex.htmlto access them.stats.wasmruns directly withwasmtimeorwasmedge; no HTML is needed.
Project Skeletonβ
mkdir -p ~/Desktop/dev/wasm/{browser,wasi}
A) Browser Example - JS β Go (syscall/js)β
Goal: Build a Go function that runs in the browser and can be called from JavaScript. We'll create a simple "mean" (average) calculation function that JavaScript can invoke.
1) Initialize Moduleβ
cd ~/Desktop/dev/wasm/browser
go mod init browser
2) main.go (Browser target - syscall/js)β
//go:build js && wasm
package main
import "syscall/js"
// Callable from JS as mean([1,2,3])
func mean(this js.Value, args []js.Value) any {
if len(args) == 0 || args[0].Length() == 0 {
return js.ValueOf(0)
}
arr := args[0]
n := arr.Length()
sum := 0.0
for i := 0; i < n; i++ {
sum += arr.Index(i).Float()
}
return js.ValueOf(sum / float64(n))
}
func main() {
js.Global().Set("mean", js.FuncOf(mean))
// Keep the program alive
select {}
}
3) Compile (Browser target)β
GOOS=js GOARCH=wasm go build -o main.wasm
4) Copy wasm_exec.jsβ
The location of wasm_exec.js depends on your Go version. The command below handles both old and new locations:
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" . 2>/dev/null || \
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
5) index.htmlβ
<!doctype html>
<html>
<body>
<h3>Go + WASM (browser)</h3>
<script src="wasm_exec.js"></script>
<script>
console.log("Script started...");
const go = new Go();
console.log("Go object created");
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject)
.then(r => {
console.log("WASM loaded");
console.log("WASM instance:", r.instance);
console.log("Go import object:", go.importObject);
try {
const result = go.run(r.instance);
console.log("Go.run result:", result);
console.log("Go runtime started");
const m = mean([1, 2, 3, 6, 8]);
document.body.insertAdjacentHTML(
"beforeend", `<p>mean: ${m}</p>`
);
console.log("mean:", m);
} catch (runError) {
console.error("Go.run error:", runError);
document.body.insertAdjacentHTML(
"beforeend", `<p>Go.run error: ${runError}</p>`
);
}
})
.catch(err => {
console.error("WASM loading error:", err);
document.body.insertAdjacentHTML(
"beforeend", `<p>WASM error: ${err}</p>`
);
});
</script>
</body>
</html>
6) Runβ
npx serve . -p 8080
# Browser: http://localhost:8080
What did we learn?
- In the browser target, we used
syscall/jsto expose Go functions to JavaScript.wasm_exec.jsprovides the Go runtime bridge that allows WASM to run in the browser.- The Go function (
mean) becomes available in JavaScript's global scope aftergo.run()executes.- Network/file operations in browser WASM must use browser APIs (Fetch, File API) via
syscall/js, not direct system calls.
B) WASI / Edge (Server) Example - CLI Applicationβ
Goal: Build a simple statistical calculation CLI (mean, std, z-score) for edge/server with WASI.
1) Initialize Moduleβ
cd ~/Desktop/dev/wasm/wasi
go mod init wasi-demo
2) main.go (WASI target - os.Args, stdout)β
//go:build wasip1 && wasm
package main
import (
"fmt"
"math"
"os"
"strconv"
)
func meanStd(values []float64) (float64, float64) {
if len(values) == 0 {
return 0, 0
}
sum := 0.0
for _, v := range values {
sum += v
}
mean := sum / float64(len(values))
var variance float64
for _, v := range values {
d := v - mean
variance += d * d
}
std := math.Sqrt(variance / float64(len(values)))
return mean, std
}
func main() {
if len(os.Args) < 2 {
fmt.Println("usage: stats <n1> <n2> ...")
fmt.Println("example: stats 10 12 11 50 9")
return
}
var vals []float64
for _, a := range os.Args[1:] {
if f, err := strconv.ParseFloat(a, 64); err == nil {
vals = append(vals, f)
} else {
fmt.Printf("skip %q: %v\n", a, err)
}
}
mean, std := meanStd(vals)
fmt.Printf("mean=%.4f std=%.4f\n", mean, std)
const zThresh = 2.0
for i, x := range vals {
z := 0.0
if std > 0 {
z = (x - mean) / std
}
outlier := math.Abs(z) >= zThresh
fmt.Printf("[%d] x=%.2f z=%.2f outlier=%v\n",
i, x, z, outlier)
}
}
3) Compile (WASI)β
GOOS=wasip1 GOARCH=wasm go build -o stats.wasm
Want a smaller file? (Optional):
brew install tinygo
tinygo build -o stats.wasm -target=wasi .
4) Runβ
# Wasmtime:
wasmtime run stats.wasm -- 10 12 11 50 9
# WasmEdge:
wasmedge stats.wasm 10 12 11 50 9
Example output:
mean=18.4000 std=15.8316
[0] x=10.00 z=-0.55 outlier=false
[1] x=12.00 z=-0.43 outlier=false
[2] x=11.00 z=-0.49 outlier=false
[3] x=50.00 z=2.09 outlier=true
[4] x=9.00 z=-0.62 outlier=false
What did we learn?
- In the WASI target,
os.Argsand stdout work naturally.- Network sockets in WASI are still limited; command-line arguments and standard I/O are practical.
- This
.wasmfile can run unchanged in an edge gateway, serverless WASM platform, or as a sidecar.
π Wasmtime vs WasmEdge - Quick Comparisonβ
| Feature | Wasmtime | WasmEdge |
|---|---|---|
| Focus | General-purpose, close to WASI standards | Cloud/edge-focused, microservices/inference |
| Performance | Excellent, stable | Excellent, advantageous for some AI/HTTP workloads |
| Extensions | WASI | HTTP/TLS, AI (OpenVINO/ONNX) extensions |
| Production Use | Broad (CLI, plugins, sandbox) | Edge platforms, serverless WASM |
| Ecosystem | Bytecode Alliance community | Strong ties to CNCF/Cloud ecosystems |
| Use Case | General-purpose, simple WASI apps | Edge services, AI inference, HTTP calls |
Summary:
- Wasmtime is great for simple CLI/agents and standard WASI applications.
- WasmEdge is more practical for edge microservices, AI inference, or HTTP/TLS workloads.
π§ FAQβ
Why are WASM files secure?
Sandboxed. They only operate with explicitly granted capabilities; no default access.
What can I do with WASI?
Read arguments, write to stdout/err, access files/clock (if permitted), and interact with the host via standard I/O.
Go vs TinyGo?
TinyGo produces much smaller .wasm files (often 10-100x smaller), ideal for edge deployment. Some runtime features may be limited; choose based on needs.
Network/file access in the browser?
Browser security requires using Fetch or File API via JS; bridge with syscall/js.
When should I use browser WASM vs WASI?
- Browser WASM (
GOOS=js): Interactive web applications, client-side computation, performance-critical web features. - WASI (
GOOS=wasip1): Edge functions, serverless, CLI tools, microservices, IoT devices.
π Summaryβ
WASM enables the "compile once, run securely anywhere" paradigm in software development.
With Go, building portable, fast, and isolated applications for both browser and edge environments is now practical.
This approach makes systems more modular, secure, and platform-independent.
π Referencesβ
-
TinyGo Project. (2024). TinyGo: Go compiler for small places. https://tinygo.org/docs/guides/webassembly/
-
Wasmtime Documentation. (2024). Fast, secure, and standards-compliant WebAssembly runtime. https://docs.wasmtime.dev/
-
WasmEdge Project. (2024). WasmEdge: Lightweight, high-performance WebAssembly runtime for cloud-native, edge, and AI applications. https://wasmedge.org
-
WebAssembly System Interface (WASI) Specification. (n.d.). Bytecode Alliance. https://github.com/WebAssembly/WASI