skip to content

What does building with GOOS=js GOARCH=wasm produce, and why does the page also need wasm_exec.js?

level: juniorimportance: must knowfreq 35%

answer

  1. the module is not self-contained
  2. it declares imports someone must supply
  3. the toolchain ships the glue
  4. copy it from GOROOT on every build
  5. new Go(), then go.run(instance)

basics

~20 s

It produces a .wasm module, not a standalone program. The module declares imports for the host functions the Go runtime needs, and wasm_exec.js is the JavaScript glue that ships with the toolchain, supplies those imports and starts the program.

solid answer

~40 s

The build emits a WebAssembly module, typically `main.wasm`, and that module is not self-contained. The Go runtime needs host services — time, randomness, writing to stdout, and the bridge to JavaScript objects — and the module declares them in its import section. `wasm_exec.js` is the glue that ships with the Go toolchain (under GOROOT, in `lib/wasm`) and provides exactly that import object. A page does `const go = new Go()`, instantiates the module with `go.importObject`, then calls `go.run(instance)`, which runs `main`. Copy the glue from the same toolchain that produced the module: the import set is an internal contract between the runtime and its glue, not a stable API, so a mismatched pair fails at instantiation. Serve the file as `application/wasm` if you want streaming instantiation.

code

text · 2 lines
text
GOOS=js GOARCH=wasm go build -o web/main.wasm ./cmd/validator
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" web/

go deeper

for a junior

Be ready to name the two artefacts a browser needs: the .wasm module you built and the wasm_exec.js glue from the toolchain, plus the three lines that instantiate and run it.

for a middle

Explain why the module has an import section at all, what the glue puts in it, and why the glue and the module must come from the same Go release.

for a senior

Show that you would make copying the glue a build step in CI, serve the module compressed with the right content type, and know that stdout and fatal errors land in the browser console.

for a principal

Own the consequence: the whole Go runtime is in that download, and the team that owns the page's performance budget pays for it. Be able to say when that is worth it.

## What the build actually emits `GOOS=js GOARCH=wasm go build -o main.wasm ./cmd/validator` compiles your Go program — runtime, garbage collector, scheduler and all — into a single WebAssembly module. It is not an executable in the operating-system sense. A WebAssembly module is a sandboxed unit of code with its own linear memory, a list of **exports** (things the host may call) and a list of **imports** (things the host must supply before the module can even be instantiated). That import list is the whole reason a second file exists. ## Why the module cannot stand alone WebAssembly by itself has no clock, no random source, no console, no files and no access to the page. A Go program needs all of those: `time.Now`, the runtime's random seed, `fmt.Println`, and — on this target — the ability to reach JavaScript objects through the `syscall/js` package. On a normal Linux build those come from system calls. On this target they come from functions the *embedder* passes in at instantiation time. If nothing supplies them, `WebAssembly.instantiate` rejects with a link error before a single Go statement runs. ## What wasm_exec.js is `wasm_exec.js` is a small JavaScript file distributed with the Go toolchain, found under `$(go env GOROOT)/lib/wasm/`. It defines a global `Go` class. An instance of that class carries two things: `go.importObject`, the object of host functions the module imports, and `go.run(instance)`, which sets up argv and environment, calls the module's start function and returns a promise that resolves when Go's `main` returns. A minimal page therefore does three things: fetch the module, instantiate it with `go.importObject`, and call `go.run` on the resulting instance. The glue also wires the program's standard output and standard error to the browser console, buffering bytes and flushing a line at a time — which is why `fmt.Println` and Go's fatal-error output show up in devtools rather than vanishing. ## Version coupling is the trap The set of functions in `go.importObject`, and the memory layout the glue reads values out of, are private implementation details shared between the runtime and the glue. They change between Go releases. Copying `wasm_exec.js` out of a blog post, vendoring it once and forgetting it, or having CI build with a newer toolchain than the checked-in glue, all produce the same symptom: instantiation fails with a missing- or mismatched-import error, or the module starts and then misbehaves. The fix is procedural rather than clever — copy the glue as a build step, from the toolchain that just compiled the module. ## Size, and what you are shipping The whole Go runtime is in the bundle, so even a trivial program lands around a couple of megabytes uncompressed. It compresses well — serving it gzipped or brotli-compressed is not optional in production — but it is still a large asset by front-end standards, and that cost belongs to whoever owns the page's performance budget. ## What you do not get This target is a browser environment, not a small Linux. There are no OS threads: every goroutine runs on the one execution thread the module has, shared with the page's event loop. There are no sockets, so `net.Dial` does not work; the `net/http` client is instead backed by the browser's fetch API. Most of `os` is unavailable or stubbed in a browser — there is no real filesystem to open. ## The other wasm target Go has a second WebAssembly target, `GOOS=wasip1 GOARCH=wasm`, aimed at WASI hosts outside the browser. That one needs no `wasm_exec.js` at all, because the host runtime supplies the standard WASI imports itself. Browsers do not implement WASI, so `GOOS=js` remains the browser target.

  • Why must wasm_exec.js come from the same toolchain that compiled the module?
    The import object and the memory layout the glue reads values from are private contracts between the Go runtime and its glue, and they change across releases. A stale copy usually fails at instantiation with a missing or mismatched import, and can otherwise misread values at run time. Treat copying it as a build step, not a one-time vendoring.
  • Where does fmt.Println output go when the program runs in a browser?
    The glue implements the write path for standard output and standard error, buffers the bytes and flushes each completed line to the browser console. So `fmt.Println` lands in devtools, and so does a runtime fatal error with its goroutine dump — that console is your only log sink on this target.
  • Can a browser run a module built with GOOS=wasip1 GOARCH=wasm instead?
    Not directly. That target emits WASI preview 1 imports, and browsers do not implement WASI; you would have to supply a WASI shim yourself. The supported browser target is GOOS=js GOARCH=wasm, which is what the toolchain's glue is written for.

The .wasm file is a cassette and wasm_exec.js is the tape deck: the tape carries no motor, and a deck from a different generation will not play it.

saying these in an interview costs you the question

  • Thinks the .wasm file runs in a page on its own
  • Copies wasm_exec.js from a tutorial and never refreshes it
  • Expects a Go wasm bundle to be a few hundred kilobytes
  • Thinks the browser target needs cgo or a C toolchain
  • Believes a browser can load a native Go binary