gc Compiler vs gccgo vs TinyGo

Go the language is defined by the Go specification, but it has more than one implementation. Three matter in practice: gc, the official compiler bundled with the Go distribution and the subject of most of the Go Internals MOC; gccgo, an alternative Go front end bolted onto the GCC back end; and TinyGo, an independent LLVM-based compiler aimed at microcontrollers and WebAssembly. They compile the same language but make sharply different trade-offs in compile speed, runtime size, optimization, platform reach, and language-feature completeness. Choosing wrongly produces real pain — a TinyGo target that silently drops recover, or a gccgo build stuck on Go 1.18 semantics. This note compares the three so you know which one is running your code and why it behaves as it does.

Mental Model

A useful way to hold the three: gc optimizes build speed and the Go runtime’s needs; gccgo optimizes reusing GCC’s mature optimizer and platform reach; TinyGo optimizes tiny output binaries for hardware where a megabyte is a lot. The first is a Go-native toolchain written in Go; the second and third borrow a mature C/C++ back end (GCC and LLVM respectively) and pay for it with a different runtime, a different calling convention, and — critically — a lag behind the language spec.

graph TD
    SPEC[Go language spec<br/>go.dev/ref/spec]
    SPEC --> GC[gc compiler<br/>cmd/compile, written in Go]
    SPEC --> GF[gofrontend<br/>C++ Go front end]
    GF --> GCCGO[gccgo<br/>gofrontend + GCC back end]
    GF --> GOLLVM[gollvm<br/>gofrontend + LLVM back end]
    SPEC --> TG[TinyGo<br/>own front end + LLVM back end]

    GC --> O1[fast incremental builds,<br/>full Go runtime, current spec]
    GCCGO --> O2[GCC optimizer + arch reach,<br/>lags spec, dynamic libgo runtime]
    TG --> O3[tiny whole-program binaries,<br/>MCU/WASM, feature subset]

Diagram: all three implement the one spec, but gccgo and gollvm share a single C++ front end (gofrontend) feeding different back ends, while gc and TinyGo each have their own front end. The insight: an implementation’s quirks come from its back end and runtime, not the language — and “borrowed back end” implementations lag the spec because the front end is the long pole.

Mechanical Walk-through

gc — the official compiler

gc is cmd/compile in the Go source tree, invoked by the go command. Its name is simply short for “Go compiler” — it has nothing to do with the garbage collector despite the acronym collision; this is one of the most common confusions in Go (Go FAQ). It is the compiler the entire rest of the Go Internals MOC dissects: a pipeline of parsing → types2 type checking → IR construction → middle-end optimization (inlining, escape analysis) → walk → SSA → machine-code generation (cmd/compile README).

gc was originally written in C “because of the difficulties of bootstrapping — you need a compiler to set up a compiler” (Go FAQ). The Go 1.5 release (August 2015) converted the compiler and runtime from C to Go using automatic translation tools, “removing the last vestiges of C code from the Go code base” (Go 1.5 release notes). Since then gc is self-hosting — building Go 1.N needs an earlier Go compiler as a bootstrap, not a C toolchain.

gc deliberately does not use LLVM. The Go team judged LLVM “too large and slow” and wanted control over things LLVM’s C-oriented design did not offer: Go’s growable goroutine stacks, the GC’s precise write barriers, and Go’s own register-based ABI (Go FAQ). The trade-off is explicit: gc does not match a mature optimizer like GCC’s or LLVM’s on raw scalar-code optimization, but it compiles fast, integrates the runtime tightly, and tracks the spec exactly. It is current with Go 1.26 by definition — gc is the reference.

gccgo — the GCC front end

gccgo is “a new frontend for GCC, the widely used GNU compiler” (gccgo setup doc). The Go-specific front end, gofrontend, is written in C++ and was started independently by Ian Lance Taylor in May 2008 from the draft Go spec (Go FAQ). It produces GCC’s intermediate representation, after which GCC’s standard optimizer and back end take over.

gccgo’s appeal is GCC’s decades-mature optimizer and GCC’s very wide architecture support — gccgo can target processors gc does not. Its costs are equally concrete. It links against the system runtime libraries and does not default to fully static linking the way gc does — you must arrange library paths (LD_LIBRARY_PATH, a -Wl,-R runtime path) or pass -static-libgo (or -static) explicitly (gccgo setup doc). It accepts GCC-style flags (-O2, -g). And, most importantly, it lags the language spec by years.

The lag is concrete and confirmed against the official mapping (gccgo setup doc): “The GCC 12 and 13 releases include a complete implementation of the Go 1.18 standard library. However, GCC does not yet include support for generics.” Generics shipped in gc with Go 1.18 in March 2022, but gofrontend never implemented them — so gccgo is stuck at pre-generics Go-1.18 semantics. The GCC 14 and 15 release-notes “Changes” pages carry no Go section at all (verified 2026-05-28), and as of December 2024 the gofrontend maintainer Ian Lance Taylor stated on the golang-dev list that he is “actively working on adding generics support to gccgo, but unfortunately I don’t have an ETA” (golang-dev thread; golangbridge forum). The practical consequence is severe and growing: any code using type parameters — and increasingly that includes generated code such as recent protoc-gen-go-grpc output and large parts of golang.org/x — simply will not compile under gccgo. For modern Go, gccgo is not a viable general-purpose compiler.

A related project, gollvm, reuses the same gofrontend C++ front end but feeds an LLVM back end through a bridge that translates gofrontend IR to LLVM IR (gollvm repo). gollvm uses the standard C/C++ calling convention rather than Go’s native ABI, and its garbage collector must scan stacks conservatively because, in the project’s own words, “gollvm does not yet support stack map generation, hence for gollvm the garbage collector has to scan stacks conservatively (which can lead to longer scan times and increased memory usage)” (gollvm README). gollvm is still in development — its README states plainly that “releases are not yet available for download,” and it inherits the same gofrontend generics gap as gccgo. It receives intermittent commits (the most recent on master was dated April 2025 as of this writing), but it is not production-ready and is prone to breaking against newer LLVM releases — for example it failed to build against LLVM release/16.x and later (golang/go#74866). It is a research/optimization vehicle, not a tool you ship with.

TinyGo — the embedded/WASM compiler

TinyGo is an independent implementation built on LLVM, targeting microcontrollers (Arduino, Raspberry Pi Pico, ESP32, nRF52, STM32, and dozens more), WebAssembly/WASI, and small desktop binaries (TinyGo compiler internals). Its central architectural difference from gc is whole-program compilation: “A whole program is compiled in a single step, without intermediate linking” (TinyGo differences-from-Go). This contrasts with gc’s package-at-a-time separate compilation. Whole-program compilation enables aggressive dead-code elimination and compile-time evaluation — TinyGo “computes global variables during compilation whenever feasible,” cutting startup time and flash usage — at the cost of slow, non-incremental builds.

TinyGo also represents interfaces differently: a simple {typecode, value} pair, with method calls “looked up where they are used” rather than via gc’s precomputed itable function-pointer list — a deliberate trade favoring smaller binaries over interface-call speed (TinyGo differences-from-Go).

The price is a language and library subset. Per the TinyGo language-support reference (TinyGo lang support): goroutines, channels, defer, and interfaces work; garbage collection “generally works fine, but may work not as well on very small chips (AVR) and on WebAssembly”; reflect is re-implemented and “most of it works, but some parts are not yet fully supported”; maps work but “may be slower than you expect”; cgo is only partially supported (#cgo statements partially supported). The sharpest limitation: runtime panics cannot be recovered from — divide-by-zero and nil-pointer dereferences are not catchable by recover, and recover itself is unsupported on WebAssembly. Code that relies on recover to contain panics will behave differently under TinyGo.

Side-by-Side Comparison

Dimensiongc (official)gccgoTinyGo
Back endown (cmd/compile, in Go)GCCLLVM
Front endown (syntax/types2)gofrontend (C++)own (in Go)
Compilation modelpackage-at-a-time, separateper-translation-unit (GCC)whole-program, single step
Build speedvery fast incrementalslowerslow, non-incremental
Spec currencyreference — Go 1.26stuck pre-generics (Go 1.18 stdlib, no type params)tracks closely but feature subset
Runtime / linkingown runtime, static by defaultlibgo + system libc, dynamic by defaultminimal embedded runtime
Sweet spotservers, CLIs, infraexotic CPU architecturesmicrocontrollers, WASM
recover from runtime panicyesyesno

Common Misunderstandings

gc stands for garbage collector.” It does not. gc is the Go compiler; the garbage collector is a separate runtime component. The acronym collision is purely coincidental and is the single most common confusion about the toolchain (Go FAQ).

“gccgo produces faster code than gc.” Sometimes, on optimization-heavy scalar code, because GCC’s optimizer is more mature. But gccgo lags the spec (no generics), links against glibc, and is not the reference — for the overwhelming majority of modern Go programs gc is the only correct choice, and it has closed much of the optimization gap with PGO and steady SSA improvements.

“TinyGo is just gc with smaller binaries.” No — it is a separate implementation with a different compilation model, a different interface representation, and a feature subset. Code that compiles and passes tests under gc can break under TinyGo, most notably anything depending on recovering from runtime panics or on full reflect. Always test on the real target.

“gccgo and gollvm are the same project.” They share the gofrontend C++ front end but differ in back end: gccgo uses GCC, gollvm uses LLVM. gollvm additionally uses the C/C++ calling convention and conservative stack scanning, and is still in development with no downloadable releases — a research/optimization vehicle, not something to ship.

“Picking a different compiler is a routine choice.” For server and CLI software it is not — gc is the default for a reason. Reaching for gccgo or TinyGo is driven by a hard constraint: a CPU gc cannot target (gccgo), or a flash/RAM budget gc binaries blow (TinyGo).

Alternatives and When to Choose Them

Choose gc — the default — for essentially all server software, command-line tools, and cloud infrastructure. It is the reference implementation, current with the spec, fast to build, and the one the rest of the Go Internals MOC describes. If you have no specific reason to do otherwise, you are using gc.

Choose gccgo only when you must target a processor architecture the official toolchain does not support, or when integrating tightly with a GCC-based build, and you can live with its spec lag — meaning your code does not need generics or other post-1.18 features. This is a niche, shrinking choice.

Choose TinyGo when the target is a microcontroller or WebAssembly and binary size is a hard constraint that gc cannot meet — gc’s smallest binaries are far too large for an 8-bit AVR. Accept up front that you are programming against a Go subset: verify your dependencies and your use of reflect, cgo, and recover against the TinyGo support matrix before committing.

Historically other implementations existed (the C-based pre-1.5 gc, llgo, experimental ports); they are not relevant choices today. The live landscape is the three above.

Production Notes

In practice almost all production Go runs on gc: the Docker/Kubernetes/Prometheus cloud-native stack is built with it, and CI pipelines pin a gc toolchain version via GOTOOLCHAIN (Go Toolchain Management). gccgo’s main remaining role is as a bootstrap option and for the handful of architectures GCC reaches that gc does not — it is also one of the four documented ways to obtain a bootstrap toolchain when building Go from source. TinyGo has carved out a genuine, growing niche: it is widely used for embedded Go (sensor firmware, hobbyist boards) and for compact WebAssembly modules, including WASI plugins, where gc’s WASM output is comparatively large. The decisive operational rule when not on gc: test on the actual implementation and the actual target, because the behavioral differences — gccgo’s spec lag, TinyGo’s unrecoverable panics and partial reflect — are silent until they bite at runtime.

See Also