I had a small Go service I wanted running on an ARM box in the corner of the room. The obvious approach, building it on the box itself, is fine until you actually try it and remember the thing has 512MB of RAM and the patience of a saint is required while the compiler thrashes swap.
So I built it on my laptop instead. This is the bit Go is genuinely brilliant at, and it took me longer to believe it than to do it.
GOOS=linux GOARCH=arm GOARM=7 go build -o myservice .
That's it. No cross-toolchain to install, no arm-linux-gnueabihf- prefix on anything, no sysroot. scp the binary across, run it, done. After years of fighting C cross-compilation this felt like cheating.
the GOARM trap
The one variable that actually matters here is GOARM, and it's the one that's easy to get wrong. It selects the floating-point ABI. Set it too high for your hardware and you get a binary that builds cleanly, copies across happily, and then dies on the box with illegal instruction. No useful error, just SIGILL and a core dump.
The rule of thumb:
GOARM=5is soft-float, works on basically anything including old ARMv5 kitGOARM=6for ARMv6, which is what an original Raspberry Pi wantsGOARM=7for ARMv7 with hardware VFP, which covers most of the Pi 2 / Pi 3 / BeagleBone era boards
If you're not sure what's in the box, cat /proc/cpuinfo on it and look at the model name and the CPU architecture line. When in doubt I drop to GOARM=6, lose a fraction of floating-point speed I almost never use, and stop having to think about it.
the cgo asterisk
The clean story above holds for as long as you stay in pure Go. The moment you pull in a package that uses cgo, say a SQLite driver that wraps the C library, the magic evaporates. Now you do need a cross C compiler, you need CGO_ENABLED=1, and you need to point CC at the right thing:
CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc \
GOOS=linux GOARCH=arm GOARM=7 go build -o myservice .
At that point you're back in the world of matching libc versions and hoping the target's shared libraries line up. My advice, learned the tedious way: avoid cgo entirely if you possibly can for these little boxes. A pure-Go SQLite or a pure-Go anything is worth a small performance hit when the payoff is a single static binary you can copy anywhere without a second thought.
For my service the dependency tree was clean, so the one-liner did the whole job. Binary across, systemd unit written, and it's been quietly serving since. The best deployments are the boring ones.