Go in Embedded: Writing Firmware in a Language That Doesn't Blow the Stack

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Go and Hardware: Why It's No Joke?

\n

When you hear the phrase "embedded development," you usually imagine C, assembly, and painful debugging via an LED. But the world doesn't stand still. Today, Go (or Golang) is increasingly knocking on the door of the microcontroller world. And it's not knocking for nothing; it comes with goroutines, a garbage collector, and built-in interfaces.

\n

In this article, we'll explore what embedded development in Go looks like, write real code for a hypothetical microcontroller, and figure out if it's worth the effort. Spoiler: yes, if you're tired of memory leaks and love readable code.

\n\n

Why Go, Not C? Three Pillars

\n

Classic embedded is C. But Go has three killer features that make engineers reconsider their habits:

\n
    \n
  • Memory safety: no wild pointers that can fly into space. The Go compiler is stricter than the C compiler.
  • \n
  • Goroutines: lightweight threads that fit perfectly into the event-driven architecture of microcontrollers.
  • \n
  • Cross-platform: you write code on a Mac and compile it for ARM Cortex-M. No jumping through hoops.
  • \n
\n

Of course, there's a nuance: native Go (gc) cannot compile for small MCUs. But there is the TinyGo project — a compiler that adapts Go for hardware with kilobytes of memory. That's what we'll focus on.

\n\n

Environment Setup: TinyGo and Your Board

\n

Before writing code, let's prepare the workspace. For embedded development in Go, you'll need:

\n
    \n
  1. Install TinyGo: brew install tinygo (or download from the official website).
  2. \n
  3. Install the toolchain for your board (e.g., ARM GCC for STM32).
  4. \n
  5. Connect the board via USB.
  6. \n
\n

Let's check that everything works:

\n
tinygo version\ntinygo: 0.31.0 (linux/amd64)\n
\n

Now you can compile code for any supported board: tinygo build -target=arduino-nano -o firmware.hex main.go.

\n\n

First Blink: LED in Go

\n

A classic. Connect to a pin, toggle it. Here's what microcontroller programming in Go looks like:

\n
package main\n\nimport (\n    "machine"\n    "time"\n)\n\nfunc main() {\n    // The built-in LED is usually on pin 13 (or LED_BUILTIN)\n    led := machine.LED\n    led.Configure(machine.PinConfig{Mode: machine.PinOutput})\n\n    for {\n        led.High()\n        time.Sleep(500 * time.Millisecond)\n        led.Low()\n        time.Sleep(500 * time.Millisecond)\n    }\n}\n
\n

Compile and flash to the board:

\n
tinygo flash -target=arduino-nano -port=/dev/ttyUSB0 main.go\n
\n

Voila! The LED blinks. Note: we use the machine package — this is TinyGo's hardware abstraction. It hides register manipulation behind the scenes but provides a clean Go interface.

\n\n

Working with GPIO and Interrupts

\n

In real projects, just blinking isn't enough. You need to read buttons, sensors, and react to events. In Go for embedded systems, interrupts are implemented via channels and goroutines. Example: attach a button press handler.

\n
package main\n\nimport (\n    "machine"\n    "fmt"\n)\n\nfunc main() {\n    button := machine.D2\n    button.Configure(machine.PinConfig{Mode: machine.PinInputPullup})\n\n    // Wait for a press (pin == LOW, since it's pulled up to VCC)\n    for {\n        if !button.Get() {\n            fmt.Println("Button pressed!")\n        }\n    }\n}\n
\n

But this is a busy loop. Want something more efficient? Use an interrupt via a channel:

\n
func main() {\n    button := machine.D2\n    button.Configure(machine.PinConfig{Mode: machine.PinInputPullup})\n\n    // Channel for events\n    ch := make(chan bool, 1)\n    button.SetInterrupt(machine.PinFalling,                

Blogs

Book Recommendations