Compiling with GOOS=wasip1 GOARCH=wasm
Go's WASI target turns ordinary packages into portable .wasm modules.
The compile command mirrors cross-compilation to Linux or Windows - set GOOS and GOARCH, then go build.
Understanding which flags matter, and which files (wasm_exec.js) belong to a different target, keeps your build scripts and CI matrices correct.
Summary
GOOS=wasip1 with GOARCH=wasm selects the WASI Preview 1 syscall surface.
The Go linker emits a module importing wasi_snapshot_preview1 functions instead of platform-specific syscalls.
You execute the artifact with an external WASM host (wasmtime, wazero, Spin) rather than the go command.
Release builds commonly pass -ldflags="-s -w" to strip debug metadata.
Browser-target notes about wasm_exec.js do not apply to wasip1 output.
Recipe
Quick-reference recipe card - copy-paste ready.
GOOS=wasip1 GOARCH=wasm go build -ldflags="-s -w" -o app.wasm .
wasmtime run app.wasmWhen to reach for this:
- Portable CLI tools distributed as a single
.wasmartifact - Sandboxed plugins executed inside a Go service via wazero
- Edge functions on Spin/Fermyon or compatible WASM hosts
- CI-verified builds that must not depend on native OS binaries
- Shared business logic compiled once and embedded in multiple host apps
Working Example
package main
import (
"encoding/json"
"fmt"
"os"
)
type config struct {
Name string `json:"name"`
}
func main() {
raw := os.Getenv("CONFIG_JSON")
if raw == "" {
fmt.Fprintln(os.Stderr, "CONFIG_JSON required")
os.Exit(1)
}
var cfg config
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("hello, %s\n", cfg.Name)
}go mod init example.com/wasigreet
# save main.go, then:
GOOS=wasip1 GOARCH=wasm go build -ldflags="-s -w" -o greet.wasm .
wasmtime run --env CONFIG_JSON='{"name":"Ada"}' greet.wasmWhat this demonstrates:
- Standard library packages (
encoding/json,os) compile for WASI without tags. - Environment variables are host-provided capabilities (
--envin wasmtime). - Exit codes propagate to the host process like a native CLI.
- Error output on
os.Stderrmaps to host stderr streams.
Deep Dive
How It Works
- The Go compiler lowers goroutines and GC to WASM instructions plus runtime imports.
wasip1mapsosfile operations to WASI fd APIs the host must implement._startexport is the entry the host calls after instantiating linear memory.go modand vendoring work the same as native builds; cgo is not available on this target.
Build Environment Reference
| Variable / Flag | Purpose |
|---|---|
GOOS=wasip1 | Select WASI Preview 1 syscall ABI |
GOARCH=wasm | Select WebAssembly architecture |
-ldflags="-s -w" | Strip symbol/debug tables for smaller binaries |
GOWASM=... | Historically toggled WASM features; signext/satconv ignored in Go 1.26 |
GOFLAGS=-trimpath | Reproducible builds without host paths embedded |
wasm_exec.js vs WASI
| Artifact | Target | How to run |
|---|---|---|
app.wasm | wasip1/wasm | wasmtime run, wazero embed, Spin deploy |
main.wasm + wasm_exec.js | js/wasm | Browser or Node with JS runtime shim |
Do not copy GOROOT/misc/wasm/wasm_exec.js into WASI deployment bundles.
Go Notes
// Separate browser entry files - never compile these into wasip1 builds.
//go:build js && wasm
package browseronly# Makefile pattern
build-wasi:
GOOS=wasip1 GOARCH=wasm go build -ldflags="-s -w" -o bin/app.wasm ./cmd/appGotchas
-
Assuming
go runworks - There is no WASI emulator insidego run; you must execute with a WASM host. Fix: wrapwasmtime runor wazero ingo:generate/Makefile targets. -
Filesystem writes without pre-open -
os.Create("/tmp/x")fails if the host did not map/tmp. Fix: passwasmtime run --dir /host/tmp::/tmpor configure wazeroFSConfig. -
Mixing
js/wasmfiles into WASI packages - Accidentalsyscall/jsimports breakwasip1compilation. Fix: isolate browser code behindjs && wasmbuild tags. -
Expecting full networking - WASI preview 1 does not guarantee TCP APIs on every runtime. Fix: confirm socket support or use Spin HTTP triggers.
-
Huge binaries by surprise - Full Go runtime adds megabytes even for
fmt.Println. Fix: measure size early; consider TinyGo or split hot paths. -
Stale
GOOS=wasi- Older docs referencewasi; modern toolchains usewasip1. Fix: update scripts towasip1for Go 1.21+.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
GOOS=js GOARCH=wasm | DOM/fetch interop in browsers | Headless servers or wazero embedding |
TinyGo -target=wasm | Strict size budgets | You need full reflect/generics-heavy libs |
Native GOOS=linux | Maximum performance and syscall coverage | You need sandboxed portable plugins |
Rust wasm32-wasi | Minimum binary size is paramount | Team standard is Go and stdlib coverage matters |
FAQs
Which Go version introduced wasip1?
Go 1.21 stabilized GOOS=wasip1 as the WASM server target.
Go 1.26 further optimizes WASM heap chunking and WASM instruction lowering.
Can I use cgo on wasip1/wasm?
No. Disable cgo (CGO_ENABLED=0, the default for cross builds) and keep dependencies pure Go.
How do I pass command-line arguments?
WASI maps args to os.Args.
wasmtime run app.wasm arg1 arg2 populates them like a normal process.
Does -tags affect WASI builds?
Yes. Build tags still split files; use wasip1-specific files when you need target-only code paths.
How do I debug WASM modules?
Keep a parallel GOOS=linux build for Delve, and use wasmtime run with logging for integration issues.
Unstripped DWARF builds are larger but can help advanced WASM debuggers.
Can plugins import each other?
WASM dynamic linking is host-specific.
Most teams compile one module per plugin and expose a narrow ABI (for example JSON over stdin/stdout).
What about GOARCH=wasm with GOOS=js?
That is the browser target, not WASI.
It produces different imports and requires wasm_exec.js.
Are goroutines supported?
Yes under the full Go compiler.
TinyGo also supports goroutines with a smaller scheduler, subject to its limits.
How do I shrink binaries further?
Start with -ldflags="-s -w", audit dependencies, and evaluate TinyGo.
Go 1.26 reduces runtime memory, not necessarily file size on disk.
Can I use go test with wasip1?
go test does not execute WASI tests natively.
Compile test binaries to WASM and run under wasmtime, or keep logic tests on linux/amd64 and WASM-smoke in CI.
Related
- Go on WebAssembly: Browser, WASI, and Embedded Paths - three-path overview
- WASI Host Runtimes: wasmtime, wazero & Spin - run and embed modules
- syscall/js: Go in the Browser - when you need js/wasm instead
- WASM Size Optimization: TinyGo vs gc WASM - linker and import tactics
- Go 1.26 WASM Runtime & Memory Improvements - runtime changes affecting WASI
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).