# TinyGo: a Go compiler for microcontrollers, WASM and small binaries

> TinyGo reuses the Go toolchain's libraries and LLVM to compile Go for boards like the Arduino Uno and for WASI runtimes. It keeps the Go memory model but not the whole standard library, and that trade-off decides who should adopt it.

**tinygo-org/tinygo** — Go compiler for small places. Microcontrollers, WebAssembly (WASM/WASI), and command-line tools. Based on LLVM.

- Repository: https://github.com/tinygo-org/tinygo
- Website: https://tinygo.org
- Stars: 17,790 · Forks: 1,079
- Language: Go
- License: NOASSERTION
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/tinygo-org-tinygo

## The gap TinyGo fills: Go on one core and a few kilobytes

The Go runtime assumes a general-purpose operating system: goroutine scheduling, a garbage collector tuned for servers, and a standard library that reaches into syscalls. None of that maps cleanly onto a microcontroller with a single core and no OS. TinyGo's stated goal is to bring Go to microcontrollers and small systems with a single processor core, and the README quotes Rob Pike's GopherCon 2014 remark that Go was never expected to be an embedded language. The project is aimed at firmware developers who already know Go and do not want to move to C or MicroPython to get onto a board.

The scope is deliberately narrow. The README lists three goals that shape every decision: very small binary sizes, support for most common microcontroller boards, and usability on the web through WebAssembly. It also lists non-goals, and those are more informative. TinyGo does not aim to be efficient with large numbers of goroutines, does not aim to be as fast as gc, and does not aim to compile every Go program. If your workload is a server process with thousands of concurrent connections, this is the wrong compiler and the README says so.

## How TinyGo compiles: Go libraries in front, LLVM behind

TinyGo is not a fork of the Go compiler. According to the README it reuses the libraries used by the Go language tools, the same packages that parse and type-check Go source, and pairs them with LLVM as the backend. That split explains most of the project's behaviour. The front end accepts ordinary Go syntax, so your editor, formatter and type checker keep working. The back end emits machine code for the target you name, which is how one blinky program runs on an Arduino Uno, an Adafruit Circuit Playground Express and a Seeed Studio XIAO-ESP32S3 without source changes.

The repository layout matches that description. There are directories for compileopts, compiler, transform, interp, loader and targets, and the build depends on tinygo.org/x/go-llvm plus a pinned LLVM version recorded in llvm-version.txt. The README contrasts this with emgo, another Go-for-microcontrollers effort, noting that TinyGo keeps the Go memory model, which implies garbage collection of some sort, and uses LLVM rather than emitting C. That choice is why the project can claim flexibility across very different instruction sets, and also why a TinyGo build drags in a large LLVM toolchain.

Garbage collection is the part that surprises people. Keeping the Go memory model means some collector has to exist even on a chip with kilobytes of RAM. The README lists goroutine efficiency as a non-goal while calling good goroutine support a goal, which is a fine distinction: the scheduler works, but it is not built for the workloads the standard Go runtime is tuned for.

## Installing TinyGo and flashing a first board

The README does not inline installation steps. It points to https://tinygo.org/getting-started/ for how to install TinyGo and for how to run the compiler from the project's Docker container, and the repository ships a Dockerfile for that purpose. So the canonical install path is the website, not a command in the README, and anything beyond that is platform-specific.

The Dockerfile builds the compiler on top of a prebuilt LLVM image. Its comments describe two ways to get that base image, either building it locally from Dockerfile.llvm or using the image CI publishes. The build stage updates submodules, runs make gen-device -j4 and then make build/release, and the final stage copies the built tinygo binary into a golang:1.27 base image with /tinygo/bin added to PATH.

```dockerfile
# docker build -t tinygo-llvm-build -f Dockerfile.llvm .
docker build --build-arg LLVM_IMAGE=ghcr.io/tinygo-org/llvm-22:<tag> .
```

Once tinygo is on your PATH, the smallest real program is the blinky example in the README. It imports machine and time, configures machine.LED as an output pin, then toggles it with one-second sleeps in an infinite loop. The README states that this program compiles and runs without modification on any supported board that has a built-in LED, provided you set the correct target.

```go
package main

import (
    "machine"
    "time"
)

func main() {
    led := machine.LED
    led.Configure(machine.PinConfig{Mode: machine.PinOutput})
    for {
        led.Low()
        time.Sleep(time.Millisecond * 1000)

        led.High()
        time.Sleep(time.Millisecond * 1000)
    }
}
```

Flashing is one command, and the README gives the Arduino Uno as its example. The target name is the flag that selects the board definition from the targets directory; the example path points at examples/blinky1 in the repository.

```bash
tinygo flash -target arduino-uno examples/blinky1
```

If you would rather target a WASI runtime, the README shows a second path. A function marked with //go:wasmexport is exported to the host, and the build uses -buildmode=c-shared with -target=wasip1. The README also notes you can use the same syntax as Go 1.24 and later by setting GOOS=wasip1 and GOARCH=wasm instead of passing -target.

```bash
tinygo build -buildmode=c-shared -o add.wasm -target=wasip1 add.go
GOOS=wasip1 GOARCH=wasm tinygo build -buildmode=c-shared -o add.wasm add.go
```

## Where TinyGo stops: language support and the wrong workloads

The README is unusually direct about limits. Compiling every Go program is a non-goal, and the supported language features are documented separately at https://tinygo.org/lang-support/ rather than in the README. That page is the first thing to read before porting anything, because the failure mode is not a clear error at the compiler boundary; it is discovering halfway through a port that a package you rely on is not implemented for your target.

Standard library coverage is where this bites hardest. The README says the project aims to support most standard library packages and to compile most Go code without modification, which is a statement about intent, not a guarantee. Code that depends on reflection-heavy packages, on net or net/http behaviour, or on runtime internals is exactly the kind of thing that falls outside the supported set. The README does not enumerate which packages are missing, so treat the lang-support page as the authority.

Performance is the second boundary, and the README handles it honestly. Being as fast as gc is a non-goal. It then adds a nuance worth quoting in spirit: LLVM will probably be better at optimizing certain things, so TinyGo might actually turn out to be faster for number crunching. That is the project's own hedge, not a benchmark. If you need throughput numbers for your workload, they are not in the README and you will have to produce them yourself.

Goroutines are the third. The README says being efficient while using large numbers of goroutines is a non-goal, while good goroutine support is a goal. A program with a handful of cooperating goroutines on a Cortex-M part is the intended shape. A program that spins up thousands is not, and no amount of LLVM optimization changes the memory budget of the chip underneath.

## TinyGo compared with MicroPython and with plain Go

The README itself supplies the comparison that motivated the project: if Python can run on microcontrollers, then certainly Go should be able to run on even lower level micros. That framing puts TinyGo next to MicroPython rather than next to the standard Go toolchain. MicroPython gives you an interpreter and a REPL on the board, which is excellent for interactive work and quick iteration, at the cost of runtime interpretation and a dynamic type system. TinyGo compiles ahead of time to machine code through LLVM, so there is no interpreter on the device and the type checking happens at build time. The cost is a heavier build toolchain on your workstation and a compile-flash cycle instead of a live prompt.

The comparison with Go itself is sharper, and the README states the trade directly. TinyGo reuses the Go language tools' libraries but replaces the backend with LLVM, and it does not aim to compile every Go program or to match gc on speed. So the choice is not which compiler is better; it is whether your program fits the subset. A CLI tool that needs the full standard library is a job for the normal Go toolchain. A firmware image that needs to fit in flash is a job for TinyGo. The README's own goal list, small binary sizes and not paying for what you do not use, is the clearest statement of which side of that line the project is on.

For WebAssembly the picture is less about substitution. TinyGo programs are documented as running in Fastly Compute, Fermyon Spin, wazero and many other WebAssembly runtimes, so the WASI target is about producing a component small enough and predictable enough for those hosts. That is a different motivation from the microcontroller case, where the constraint is RAM and flash rather than a host runtime's startup cost.

## Maintenance cadence, releases and what the licence covers

The repository is not archived and the last push was on 2026-09-20, so the project is being worked on now. The release history shows a steady cadence: v0.41.0 on 2026-04-21, a patch release v0.41.1 the next day, and v0.42.0 on 2026-09-01. The default branch is dev, which means the branch you would build from is not the branch releases are cut from, and the Dockerfile's build stage reflects that by running git submodule update --init before make gen-device and make build/release.

The upgrade cost is dominated by LLVM, not by TinyGo's own code. The repository pins an LLVM version in llvm-version.txt and depends on tinygo.org/x/go-llvm at a specific pseudo-version, and the Dockerfile expects a prebuilt LLVM image such as ghcr.io/tinygo-org/llvm-22. Building the compiler from source therefore means matching the LLVM build the project expects, which is why the documented path is the published image rather than a bare make. If you consume TinyGo as a binary or container, that cost is someone else's; if you build it yourself, it is yours on every upgrade.

On licensing, the README states the project is licensed under the BSD 3-clause license, the same licence as the Go project itself. It also states that some code has been copied from the LLVM project and is therefore licensed under a variant of the Apache 2.0 license, indicated in the headers of those files. The repository's LICENSE file is the authoritative text, and the GitHub metadata for this repository reports the licence as NOASSERTION, meaning the automated classifier did not resolve it. If licence compatibility matters to your organisation, read LICENSE and the per-file headers rather than relying on the repository label.

## Conclusion

Adopt TinyGo if you want Go syntax and the Go memory model on a supported board or a WASI runtime, and you can live with the language-support matrix. Do not adopt it if you need every package in the standard library, heavy goroutine workloads, or a board that is not in the target list. Before committing, check your board at tinygo.org/docs/reference/microcontrollers/ and read tinygo.org/lang-support/ for the packages you depend on; the README states that compiling every Go program out there is a non-goal.

## FAQ

### What is TinyGo?

TinyGo is a Go compiler intended for small places such as microcontrollers, WebAssembly and command-line tools. It reuses the libraries used by the Go language tools alongside LLVM to provide an alternative way to compile Go programs.

### How do I install TinyGo?

The README directs you to the getting started instructions at tinygo.org/getting-started for how to install TinyGo and how to run the compiler using the project's Docker container. The repository also ships a Dockerfile that builds the compiler on top of a prebuilt LLVM image.

### How do I use TinyGo to compile for a microcontroller?

Set the compiler target for your board and flash the program. The README's example compiles and flashes an Arduino Uno with tinygo flash -target arduino-uno examples/blinky1, and states that the same blinky program runs unmodified on any supported board with a built-in LED once the correct target is set.

### How do I compile Go to WebAssembly with TinyGo?

Build with the wasip1 target and c-shared build mode, for example tinygo build -buildmode=c-shared -o add.wasm -target=wasip1 add.go. The README also notes you can use the same syntax as Go 1.24 and later by setting GOOS=wasip1 and GOARCH=wasm instead of passing -target.

### Does TinyGo use LLVM?

Yes. The README states that TinyGo reuses the libraries used by the Go language tools alongside LLVM, and the project's own reasoning contrasts this with emgo, which emits C instead of using LLVM internally.

## Sources

- [Issues](https://github.com/tinygo-org/tinygo/issues)
- [Project website](https://tinygo.org)
- [README](https://github.com/tinygo-org/tinygo/blob/dev/README.md)
- [Releases](https://github.com/tinygo-org/tinygo/releases)
- [tinygo-org/tinygo on GitHub](https://github.com/tinygo-org/tinygo)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tinygo-org-tinygo
