rgoussu@goussu: ~/library/go/compilers-and-runtimes
~/library/go/compilers-and-runtimes cat tinygo.md

TinyGo deep dive

# The LLVM-based Go compiler for microcontrollers and WASM — dead-code elimination, selectable GC, the machine package, and its limits.

Conceptsaved 2026-08-09 #go#runtime#compilers#embedded#wasm

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=none drops 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 of net, 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/json works in recent releases but verify your dependency tree.
  • Stdlib gaps: no os/exec, incomplete net (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

Citations

[1] TinyGo documentation [2] TinyGo language support (reflection, stdlib) [3] proxy-wasm Go SDK