I have a small ARM box in the cupboard doing a useful but dull job, and I wanted to put a little Go service on it. Building on the box itself is an option I rejected immediately: it has the compute of a damp flannel and would take minutes to do what my laptop does in seconds. So, cross-compilation. This is the single best thing about Go and I will not be argued out of it.
The headline is genuinely this short:
GOOS=linux GOARCH=arm GOARM=7 go build -o myservice .
Set three environment variables, build, copy the binary across, run it. No toolchain to install, no sysroot, no Docker image full of ARM cross-compilers. For a pure-Go program it really is that easy, and after years of fighting C cross-toolchains it still feels like cheating.
But there are two traps that got me, and they're worth writing down because I will absolutely forget them by the next time.
the GOARM trap
GOARCH=arm is not specific enough on its own. ARM has had several floating-point situations over the years, and GOARM is how you pick. The values that matter in practice:
GOARM=5for the oldest soft-float kit, no hardware floating point.GOARM=6for things like the original Raspberry Pi.GOARM=7for anything modern-ish with VFPv3, which is most boards you'd buy now.
Get this wrong and the symptom is not a friendly error. The binary copies across fine, you run it, and it dies with illegal instruction or just refuses to start, because you've handed the CPU floating-point instructions it has never heard of. The first time I hit this I spent a good twenty minutes convinced the binary was corrupt in transit before I checked what I was actually targeting.
The way to find out what the box wants, from on the box:
$ uname -m
armv7l
$ cat /proc/cpuinfo | grep -i features
Features : half thumb fastmult vfp edsp neon vfpv3 ...
armv7l and vfpv3 in the features means GOARM=7 is what you want. If you see an older armv6 and no vfpv3, drop to 6. It is five seconds of checking that saves you a baffling crash later.
then CGO ruins it
Everything above is true right up until your program, or one of its dependencies, uses cgo. The moment any C is involved, Go cannot conjure a C cross-compiler out of thin air, and the lovely three-variable trick stops working. You'll see it fail at link time, or CGO_ENABLED will quietly flip on because some import needs it and the build will demand a C toolchain you don't have.
The most common culprit by a mile is the standard library's net and os/user packages, which can pull in cgo for DNS resolution and user lookups. The fix, for most server workloads, is to just say no:
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o myservice .
CGO_ENABLED=0 forces the pure-Go implementations of those packages. You lose the C-backed resolver, which for a service that talks to a couple of known hosts is a price worth nothing. As a bonus you get a fully static binary, which means it'll run on a box with a different libc, or no libc to speak of, and you stop caring about the target's userland entirely. For shipping to a minimal ARM image this is exactly what you want.
If you genuinely need cgo, because you're binding to some C library, then the easy life is over and you're into installing an ARM cross-toolchain and setting CC to point at it:
CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc \
GOOS=linux GOARCH=arm GOARM=7 go build -o myservice .
That works, but now you've got the gcc cross-compiler to install and keep happy, and you've imported all the joy of C cross-builds back into your nice clean Go workflow. My honest advice is to avoid it if you possibly can. Ninety percent of the time, CGO_ENABLED=0 is both the fix and an upgrade.
the bit that actually matters
Once it builds, verify before you celebrate. file will tell you what you actually produced:
$ file myservice
myservice: ELF 32-bit LSB executable, ARM, EABI5 ... statically linked ...
ARM, 32-bit, statically linked. That's the binary I wanted. Copy it over with scp, mark it executable, run it. If it starts, you're done; if it says illegal instruction, you got GOARM wrong, go back two sections.
The whole thing took me longer to write up than to do. That's rather the point. Go's cross-compilation is the feature that quietly makes a whole class of "but I haven't got the right machine" problems disappear, and the two traps above are the only ones I've ever needed to keep in my head. The damp-flannel box in the cupboard is running its service, and I never once had to wait for it to compile anything.