skip to content

What does GOOS=wasip1 GOARCH=wasm produce, and what can that module no longer do at run time?

level: middleimportance: should knowfreq 24%

answer

  1. no JavaScript anywhere in this one
  2. the host runtime supplies the imports
  3. runs and exits like a CLI tool
  4. authority arrives as preopened directories
  5. preview 1 cannot dial out

basics

~20 s

It produces a WebAssembly module for WASI preview 1 hosts; the host runtime supplies the imports, so no JavaScript glue. The program sees only directories the host preopened, cannot dial out, and has no threads or subprocesses.

solid answer

~50 s

`GOOS=wasip1 GOARCH=wasm go build` emits a `.wasm` module whose imports are the WASI preview 1 interface, so any WASI host runtime can execute it — no `wasm_exec.js` and no browser involved. By default it is a WASI *command*: it exports a start function, runs `main`, and exits like a CLI tool. The sandbox is capability-based, which is where behaviour differs from a native build. The filesystem is not global: the program can only reach paths under directories the host explicitly preopened, and anything else fails as if it did not exist. There is no outbound dialling, because preview 1 has no socket-connect API, so `net.Dial` fails; a server can only accept on a listening socket the host handed it. There are no OS threads, no `os/exec`, and no signals. Arguments, environment variables, the clock and randomness all come from the host.

code

text · 1 line
text
GOOS=wasip1 GOARCH=wasm go build -o validate.wasm ./cmd/validate

go deeper

for a junior

Recall that this target produces a WebAssembly module for non-browser WASI hosts, needs no JavaScript glue, and by default runs and exits like a command-line program.

for a middle

Explain capability-based access: the module only sees directories the host preopened, cannot dial out, and has no threads, subprocesses or signals.

for a senior

Demonstrate that you would build the target early to smoke out ambient-authority assumptions, and know that inbound serving needs a socket the host hands over while outbound calls simply are not available.

for a principal

Weigh whether a plugin surface is worth the sandbox's constraints: what the host must preopen, who owns that contract, and what happens to the library's API when capabilities have to be passed in explicitly.

## The target WASI, the WebAssembly System Interface, is a standard set of host functions — open a file, read a clock, write to a descriptor — that a WebAssembly module can import so it can behave like a program rather than a pure function. `GOOS=wasip1 GOARCH=wasm` targets preview 1 of that interface. The output is a single `.wasm` file whose import section names the WASI functions; any host runtime that implements preview 1 can load and run it. The practical contrast with the browser target is worth stating plainly. `GOOS=js` produces a module whose imports are satisfied by `wasm_exec.js`, the JavaScript glue shipped with the toolchain, and it exists to run inside a page. `GOOS=wasip1` produces a module whose imports are satisfied by the host runtime itself, and it exists to run outside a browser — as a plugin inside another process, at an edge, or as a portable CLI artefact. Browsers do not implement WASI, so the two are not interchangeable. There is no `syscall/js` on the wasip1 target. ## Command versus reactor A default build is a WASI *command*: the module exports a start function, the host calls it, `main` runs, the program exits, and the instance is spent. That is the right shape for a batch tool. If you want the host to call *into* the module repeatedly — the plugin shape — you build a reactor instead, with `-buildmode=c-shared`, which exports an initialise function and leaves the runtime alive so exported functions stay callable afterwards. ## The sandbox: what is actually denied The headline is that WASI is **capability-based**. A native process starts with ambient authority over the whole filesystem, subject only to permissions. A WASI module starts with none, and receives capabilities as preopened file descriptors from the host. So: - **Files.** `os.Open("/etc/hosts")` succeeds only if the host preopened a directory containing that path, and mapped it into the module's view. Otherwise it fails as though the path did not exist. Code that assumes a config file is somewhere absolute breaks; code that takes its paths from arguments generally survives. - **Network.** Preview 1 has no API for making an outbound connection, so `net.Dial` and anything built on it — an HTTP client, a database driver — cannot work. Accepting connections is the one direction that can work, and only when the host hands the module an already-listening socket to accept on. - **Processes and signals.** No `fork`, no `os/exec`, no signal delivery. There is no process tree to speak of. - **Threads.** Preview 1 has no threads. The Go runtime runs every goroutine on the module's single execution thread. Goroutines, channels and the scheduler all work exactly as written, and concurrency is real — but parallelism is not, so a CPU-bound goroutine starves everything else until it yields or blocks. - **Environment and arguments.** These do exist; the host passes them in, and `os.Args` and `os.Getenv` read them. Clock and randomness likewise come from host imports. ## Consequences for how you write the code Because capabilities arrive from the host, wasip1 rewards programs that take their inputs explicitly: read paths from arguments, take the working directory from a preopen, and stream input on standard input rather than reaching around the filesystem. It punishes libraries that reach for ambient resources during package initialisation — a package that opens a well-known path in `init` will fail at start-up under a host that did not preopen it, and the failure looks like a bug in your program rather than a missing capability. And because there is no dialling, a service you were hoping to port often turns out to depend on outbound calls. Discovering that early is the point of trying a wasip1 build before promising one. ## Testing `go build` produces the module, and `go test` for this target runs the compiled test binary under a WebAssembly runtime configured for the toolchain, which is the cheapest way to find out which parts of a package silently depend on ambient authority.

  • A package works natively but fails immediately under a wasip1 host. What do you suspect first?
    Ambient authority. Something is reaching for a resource nobody granted — a well-known config path opened during package initialisation, a temp directory, or an outbound connection. Under WASI the module starts with no filesystem authority at all and gets only what the host preopened, so the fix is usually to take the path or descriptor as an explicit input.
  • Can a wasip1 module serve HTTP at all?
    Only inbound, and only with help. Preview 1 has no connect API, so a client cannot dial. A server can work when the host preopens a listening socket and hands it to the module to accept on. Anything requiring the module to originate connections — calling another service, a database driver — does not.
  • Do goroutines still work on this target?
    Yes. Goroutines, channels, select and the scheduler all behave as written; what is missing is parallelism, because the port has no OS threads and everything runs on one execution thread. A tight CPU-bound loop therefore starves every other goroutine until it blocks or yields.

saying these in an interview costs you the question

  • Expects a wasip1 module to run in a browser
  • Assumes the module sees the host's whole filesystem
  • Plans outbound HTTP calls from a preview 1 module
  • Thinks wasip1 needs wasm_exec.js like the browser target
  • Expects parallel goroutines because the host has many cores