Overview
TinyGo is an alternative Go compiler built on LLVM, aimed at the two places the standard gc toolchain cannot reasonably go: microcontrollers with kilobytes of RAM, and WebAssembly where module size is the budget. It compiles real Go — goroutines, channels, interfaces, much of the standard library — but with a different compilation model and runtime, so adopting it is a porting decision, not a flag flip. It is the Go world's closest analog to a "different runtime story", much as GraalVM native-image is to HotSpot: dramatic size and start-up wins bought by giving up dynamism.
Key points
- LLVM backend: TinyGo lowers Go through LLVM, inheriting its optimiser and its bare-metal and WASM targets; compilation is whole-program.
- Aggressive dead-code elimination: only code proven reachable is compiled — the main reason binaries land in kilobytes where gc produces megabytes.
- Selectable GC:
-gc=conservative(default, mark-sweep),-gc=leaking(never free — fine for short-lived WASM calls),-gc=none(allocation is a compile/runtime error path; full manual control). - Goroutines on bare metal: a cooperative scheduler runs goroutines and channels
without an OS;
-scheduler=nonedrops even that for pure interrupt-driven code. - The machine package: board-specific access to GPIO, I2C, SPI, UART, ADC and PWM;
dozens of supported boards (Arduino, micro:bit, Raspberry Pi Pico/RP2040, many
Adafruit SAMD boards, ESP32 variants) selected by
-target. - WASM output size: small modules — commonly tens of kilobytes versus a couple of megabytes from gc — which is why it dominates plugin and filter use cases (proxy-wasm/Envoy filters, Extism-style plugins, edge runtimes).
- Subset, not superset: partial reflection support, stdlib gaps (
os/exec, parts ofnet, anything needing full OS facilities), limited cgo; check before adopting.
Details
How it differs from gc
gc compiles packages separately and links; TinyGo compiles the whole program at once
through LLVM, which enables cross-package dead-code elimination and interface
devirtualisation aggressive enough to strip most of the stdlib you don't touch. The
runtime is TinyGo's own, small enough for bare metal: no netpoller, no OS threads, a
cooperative goroutine scheduler (goroutines yield at blocking points; on WASM,
-scheduler=asyncify uses Binaryen's asyncify to suspend the stack). Stack handling, GC
and memory layout all differ from gc — behaviour that leans on gc runtime details will
not carry over.
Embedded and IoT
The machine package is the hardware API: machine.LED, machine.I2C0, pin
configuration, interrupts — with per-board definitions selected by -target=<board>.
The TinyGo drivers repository adds several hundred peripheral drivers (sensors,
displays, radios). tinygo flash builds and programs the board in one step. This makes
Go a credible MCU language for sensor nodes, badges and IoT prototypes — with the caveat
that hard-real-time work still fights the GC unless you run -gc=none and preallocate.
WASM plugins and filters
For browser or WASI targets the size argument is decisive: a plugin host loading
hundreds of modules, or a proxy hot-loading filters, cannot pay megabytes per module.
TinyGo is the standard way to write Envoy/proxy-wasm filters in Go, and the usual
choice for WASI plugin ecosystems. gc's wasip1 support is fine for running an
application under a WASM runtime; TinyGo wins when the module is an embedded extension.
Limitations to check before adopting
- Reflection: substantially supported but not complete — reflection-heavy libraries
(ORMs, some serialisation/DI code) are the first thing to break;
encoding/jsonworks in recent releases but verify your dependency tree. - Stdlib gaps: no
os/exec, incompletenet(no full TCP stack on bare metal; WASI networking is limited), and anything assuming an OS. - Concurrency model: cooperative scheduling, no parallelism — a busy loop without yields blocks everything.
- Tooling: slower whole-program builds, more limited debugging than delve on gc.
- Rule of thumb: prototype the real dependency graph early — as with GraalVM native-image, projects stall on one incompatible transitive dependency, not on the compiler.
Examples
# Build + flash a Raspberry Pi Pico (RP2040); -target selects board + machine package
tinygo flash -target=pico ./cmd/sensor
# Or just build the firmware image
tinygo build -target=pico -o firmware.uf2 ./cmd/sensor
# WASI module for a plugin host (wazero, Wasmtime, Envoy-adjacent runtimes)
tinygo build -target=wasip1 -o plugin.wasm -no-debug -gc=leaking ./cmd/plugin
# Browser WASM (pairs with TinyGo's wasm_exec.js shim)
tinygo build -target=wasm -no-debug -o web.wasm ./web
-no-debug strips DWARF (a large fraction of module size); -gc=leaking suits
short-lived plugin invocations where the host tears the instance down anyway.
Related
- Go compilers & runtimes compared — parent comparison; where TinyGo sits against gc, gccgo and gollvm.
- Building and releasing — the standard gc build/release flow TinyGo substitutes for these targets.
- GraalVM deep dive — the JVM's analogous trade: closed-world compilation for size and start-up, paid for in dynamism.
Citations
[1] TinyGo documentation [2] TinyGo language support (reflection, stdlib) [3] proxy-wasm Go SDK