Skip to main content

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

Β· 8 min read
Beste KY
Full-Stack Developer

WebAssembly with Go

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 .wasm file 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/js bridge.
  • GOOS=wasip1 produces WASI-compatible CLI/agents for edge/server.
  • TinyGo generates much smaller .wasm files 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 the syscall/js bridge.
  • 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.wasm and wasm_exec.js must be in the same directory for index.html to access them.
  • stats.wasm runs directly with wasmtime or wasmedge; 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/js to expose Go functions to JavaScript.
  • wasm_exec.js provides the Go runtime bridge that allows WASM to run in the browser.
  • The Go function (mean) becomes available in JavaScript's global scope after go.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.Args and stdout work naturally.
  • Network sockets in WASI are still limited; command-line arguments and standard I/O are practical.
  • This .wasm file can run unchanged in an edge gateway, serverless WASM platform, or as a sidecar.

πŸ†š Wasmtime vs WasmEdge - Quick Comparison​

FeatureWasmtimeWasmEdge
FocusGeneral-purpose, close to WASI standardsCloud/edge-focused, microservices/inference
PerformanceExcellent, stableExcellent, advantageous for some AI/HTTP workloads
ExtensionsWASIHTTP/TLS, AI (OpenVINO/ONNX) extensions
Production UseBroad (CLI, plugins, sandbox)Edge platforms, serverless WASM
EcosystemBytecode Alliance communityStrong ties to CNCF/Cloud ecosystems
Use CaseGeneral-purpose, simple WASI appsEdge 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​