One of the genuinely lovely things about Go is that cross-compiling is a non-event. I have a little ARM box, a Raspberry Pi, doing some monitoring duty in the cupboard, and compiling anything non-trivial on it directly is an exercise in patience. So I don't. I build on my laptop and copy the binary over. The whole process is two environment variables and a go build, and the first time it worked I genuinely didn't believe it.
The core of it is this:
GOOS=linux GOARCH=arm GOARM=7 go build -o monitor ./cmd/monitor
scp monitor pi:/usr/local/bin/
That's it. GOOS is the target operating system, GOARCH is the architecture, and the result is a static binary you can drop onto the target and run. No toolchain to install, no sysroot to assemble, no cross-gcc nonsense. Compared to the afternoons I have lost to cross-compiling C for embedded targets, this feels like cheating.
The one variable people forget is GOARM, and it matters. ARM has had several instruction set versions, and the floating point support differs between them. For a Pi 2 or 3 you want GOARM=7, which targets ARMv7 with hardware floating point. An original Pi or a Pi Zero is ARMv6, so you'd set GOARM=6. Get it wrong and the binary either won't run at all or, more annoyingly, runs slower than it should because it's emulating float operations the chip could have done in hardware. If you're on a 64-bit ARM board running a 64-bit OS, you skip GOARM entirely and use GOARCH=arm64 instead, which is the simpler case.
The thing that will trip you up is CGO. The moment your program, or one of its dependencies, links against C, the pure-Go cross-compilation story falls apart, because now you need an actual C cross-compiler for the target. The usual culprits are anything touching net with non-default resolvers, or SQLite, or some crypto libraries. The fix, where you can manage it, is to turn CGO off:
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o monitor ./cmd/monitor
With CGO_ENABLED=0 you get a fully static binary with no libc dependency at all, which is exactly what I want for something I'm scp-ing onto a minimal box. It also means the binary doesn't care which distro or libc version is on the target, which has saved me more than once. The catch is that some packages genuinely need CGO and will fail to build or behave differently without it, the standard library's DNS resolution being the classic example, so know what you're giving up before you reach for it.
My actual workflow these days is a three-line shell script that sets the variables, builds, and scps the result over. I run it from my laptop, the binary lands on the Pi, and a systemd unit picks up the new one on the next restart. The Pi never sees a compiler, never breaks a sweat, and I never wait fifteen minutes for it to grind through a build it has no business doing. For a language that gets plenty of criticism elsewhere, Go absolutely nailed this part, and I'm not too proud to say so.