What do the go:wasmimport and go:wasmexport directives do, and why are their parameter types restricted?
answer
- one directive declares, one publishes
- no body on the import side
- wasm signatures are only numbers
- text crosses as offset plus length
- a command exits, a reactor stays
basics
~20 sThey wire a Go WebAssembly module to its host without cgo: go:wasmimport binds a bodyless declaration to a host function, and go:wasmexport publishes a Go function for the host to call. Types are limited because WebAssembly signatures carry only numbers.
solid answer
~60 s`//go:wasmimport <module> <name>` sits above a Go function declaration **with no body** and binds it to a function the host supplies under that module and name; the compiler puts it in the WebAssembly import section. `//go:wasmexport <name>` sits above a normal function **with** a body and adds it to the export section so the host can call in. Together they are this target's replacement for cgo, which is not available on the WebAssembly architecture. The type restriction is not a Go decision: a core WebAssembly function signature can only carry 32- and 64-bit integers and floats, so only Go types that map onto those may appear. A string, slice, map or struct has no wire form and is passed as an offset and a length into the module's linear memory, which the host reads out. One more shape rule: a default build is a WASI command that exits when `main` returns, so exports are only useful for as long as it runs; a reactor built with `-buildmode=c-shared` keeps the runtime alive so exports stay callable.
code
go · 10 lines// The host implements this; Go only declares it, with no body.
//go:wasmimport env log_line
func logLine(ptr uint32, size uint32)
// The host can call this under the name "validate".
//go:wasmexport validate
func validate(ptr uint32, size uint32) int32 {
logLine(ptr, size)
return int32(problemCount(ptr, size))
}go deeper
Recall the split: go:wasmimport names a function the host provides and is written with no body, while go:wasmexport publishes one of your functions for the host to call.
Explain why only numeric types appear in the signatures and how a document is therefore handed over as an offset and a length into the module's linear memory.
Show that you would settle allocation and lifetime up front — who owns the buffer, who frees it, what keeps it alive — and build a reactor rather than a command when the host calls in repeatedly.
Treat the exported signatures as a versioned contract with another team: they are costly to change because both sides recompile together, so keep the surface small and let the payload encoding absorb evolution.
## Why these directives exist On every other platform, Go's escape hatch to foreign code is cgo. cgo is not available for the WebAssembly architecture, and it would not help anyway: a WebAssembly module does not link against native libraries, it *imports functions from its embedder*. The two compiler directives expose that mechanism directly. ## go:wasmimport //go:wasmimport env log_line func logLine(ptr uint32, size uint32) The directive takes two arguments — the import module name and the import field name — and must be followed by a function declaration **without a body**. There is nothing for Go to compile; the declaration is a promise that the host will provide a function under `env.log_line` with a matching signature. The compiler records it in the module's import section. If the host does not supply it, instantiation fails, which is a link error you find at start-up rather than a crash later. For a WASI build the module name is usually the WASI namespace for standard calls, or your own agreed namespace for host-specific ones — the pair is a contract you and the host owner both write down. ## go:wasmexport //go:wasmexport validate func validate(ptr uint32, size uint32) int32 Here the function has a body, and the directive adds it to the module's export section under the given name so the host can call it. This is what turns a Go module from a program the host runs into a component the host calls. ## Why the types are restricted A WebAssembly function signature is built from the core value types: 32- and 64-bit integers and 32- and 64-bit floats. That is the whole vocabulary at the boundary. So the directives accept only Go types with a direct representation in it — the sized integer and float types, and `unsafe.Pointer`, which is just an address in the module's linear memory. Everything else has no wire form. A Go string is a length and a pointer to bytes; a slice adds a capacity; a map and a struct are runtime layouts the host knows nothing about. The universal convention is therefore to pass **an offset and a length**: the caller writes the bytes into the module's linear memory, passes where and how many, and the callee reconstructs a view over them. The host can read a module's memory directly, because linear memory is an exported object, which is what makes this cheap — no serialisation, just an agreed region. That convention forces two questions you must answer explicitly: who allocates the buffer, and who keeps it alive. Typically the module exports a small allocator so the host can ask for space inside Go's memory. And the Go side must retain a reference to anything the host holds an offset into for as long as it might read it — the garbage collector cannot see a host's offset, and will happily reclaim a buffer whose only remaining “user” is outside the sandbox. ## Command versus reactor, and why it decides whether exports are usable By default a WebAssembly build is a WASI *command*: the host calls its start function, `main` runs, the program exits. Exported functions are only meaningful while that program is running, which for a batch tool means barely at all. The plugin shape is a *reactor*, built with `-buildmode=c-shared`: the module exports an initialisation function instead, the host calls it once to bring the runtime up, and the exported functions remain callable afterwards. If you are designing a plugin surface, that flag is not an optimisation, it is the difference between a design that works and one that does not. ## How this differs from the browser target In a page you would normally not touch these directives at all: `syscall/js` gives you a much richer bridge, with handles to arbitrary JavaScript objects, automatic conversion of strings and numbers, and function wrappers. The directive pair is the mechanism for hosts that are not JavaScript — another process embedding a runtime, an edge platform, a plugin host — where there is no JavaScript object graph to talk to and the contract is a flat list of numeric functions. ## Designing the boundary Because only numbers cross, the interesting design work is in the encoding you agree on. Keep the surface small: one export that takes an offset and a length and returns a status code is easier to version than a dozen fine-grained exports, and it lets you evolve the payload format — a length-prefixed encoding, or JSON in the buffer — without changing the signatures. Signatures are the part that is expensive to change, because both sides recompile together.
- How does a document written by the host reach Go, if strings cannot cross?Through the module's linear memory. The host writes the bytes into a region — usually one the module allocated and returned an offset for — then calls the export with that offset and the length. Go builds a view over those bytes. The two sides only ever exchange numbers; the memory is the payload channel.
- What keeps a buffer alive while the host still holds an offset into it?You do, explicitly. The garbage collector cannot see an offset held outside the sandbox, so a buffer whose only remaining user is the host is collectable. Keep a reference on the Go side for as long as the host may read or write it, and make the release point part of the documented protocol.
- Why is -buildmode=c-shared the right choice for a plugin?Because it builds a reactor rather than a command. A command runs main and exits, after which its exports are dead. A reactor exports an initialisation function the host calls once, and the runtime then stays up so exported functions remain callable for the life of the instance.
saying these in an interview costs you the question
- Gives a go:wasmimport declaration a function body
- Expects to pass a Go slice or struct across directly
- Reaches for cgo on the WebAssembly architecture
- Ships a plugin as a default command build and wonders why exports die
- Lets the host hold an offset into memory Go may collect