Alright, let’s get this out of the way: TinyGo is not just “Go, but smaller.” It’s a reimagining of the Go toolchain for a different class of hardware, and with that comes a set of trade-offs, quirks, and frankly, some brilliant engineering hacks. Think of it as Go’s pragmatic, slightly rebellious cousin who lives in a tiny house and is obsessed with efficiency. You’re going to use 95% of the language you know and love, but that missing 5% will keep you on your toes.

The Big One: The Go Runtime (or Lack Thereof)

This is the architectural hill everything else dies on. Standard Go has a rather beefy runtime that handles goroutine scheduling, garbage collection, and reflection. Your microcontroller, with its meager few kilobytes of RAM, would simply laugh at this concept before crashing spectacularly.

TinyGo, therefore, takes a chainsaw to the runtime. There is no concurrent garbage collector. There is no complex goroutine scheduler with work-stealing. Instead, TinyGo uses a much simpler, cooperative scheduler for goroutines. Your go statements still work, but under the hood, it’s more like a state machine that yields control on certain operations like channel communication or time.Sleep(). It’s brilliant because it gives you the feeling of concurrency without the crushing overhead. The garbage collector exists, but it’s a much simpler conservative collector. You’ll often resort to avoiding heap allocations altogether (go:noinline and stack-based tricks become your best friends) because a collection cycle on an Arduino Uno is a big deal.

Unsupported Packages and Language Features

The standard library is a buffet, and TinyGo has a much smaller plate. Packages like net/http or html/template are right out—what are you going to serve, a website to the potentiometer? You keep the fundamentals: fmt, time, strconv, and machine (TinyGo’s hardware abstraction package, which is fantastic).

Language features take a hit too. The big one is reflection. The reflect package is severely limited. This means anything that relies heavily on runtime type introspection, like encoding/json marshaling/unmarshaling arbitrary structs, is either unsupported or full of pitfalls. You have to be much more explicit. This isn’t always a bad thing; it forces you to write more predictable and efficient code.

// This might work in full Go, but it's a dangerous gamble in TinyGo.
// The reflection-based marshaling can be expensive and unpredictable.
data, err := json.Marshal(myComplexStruct)
if err != nil {
    log.Fatal(err)
}

// The TinyGo way is often to be more manual, using simpler types.
// Or, better yet, use code generation tools for marshaling.
var buf bytes.Buffer
fmt.Fprintf(&buf, "{\"value\": %d}", myStruct.Value)
// Now use buf.Bytes()

Function closures and deferred function calls are supported, but be aware they incur a overhead that might be meaningful in a tight hardware interrupt service routine (ISR). I’ve seen people avoid defer in ISRs altogether because the function call cost is too high.

The unsafe Package is Your (Sometimes) Friend

In standard Go, using unsafe is like juggling chainsaws. In TinyGo, it’s more like using a very sharp chef’s knife—still dangerous, but sometimes it’s the right tool for the job. You’ll use it to directly manipulate memory-mapped hardware registers or to create ultra-efficient data structures by bending the type system. The TinyGo drivers for devices are full of unsafe.Pointer usage to talk directly to hardware. It’s not idiomatic Go, but it’s essential embedded systems programming.

// Example: Directly accessing a memory-mapped register for an LED.
// This is platform-specific and looks like magic, but it works.
led := (*volatile.Register8)(unsafe.Pointer(uintptr(0x40000000)))
led.Set(1) // Turn LED on

Compiler Target: The .wasm Wildcard

Here’s a fun twist. While built for microcontrollers, TinyGo is also an excellent compiler for WebAssembly (WASM). The same constraints that make it good for chips (small binary size, minimal runtime) make it phenomenal for the web. This is a huge strength. You can write your core application logic in Go and compile it with TinyGo to run in a browser, sharing code with your embedded device. It’s a genuinely killer feature that the main Go compiler’s WASM target can’t match on binary size.

The Bottom Line: Mindset Shift

Working with TinyGo isn’t about writing generic Go code and hoping it fits. It’s about embracing a constrained environment. You start thinking about memory before you write a line of code. You question every allocation. You precompute values at compile time. You use channels and goroutines judiciously, appreciating their simplicity rather than fearing their overhead.

It’s a different philosophy. Full Go gives you a powerful engine and says “go build anything.” TinyGo hands you a meticulously crafted set of tools and says “alright, let’s see how clever you can be.” It’s my favorite way to write embedded software.