34.1 Static Binaries: One File, No Runtime Dependencies
Right, let’s talk about static binaries. This is one of Go’s killer features, and if you’re coming from the world of Python, Ruby, or even Java, it’s going to feel like a superpower. The promise is simple: you run go build, and out pops a single, self-contained executable file. No need to install a runtime on the server, no worrying about whether the right version of lib-whatever is installed. You just scp the file over, run it, and it works. It’s the software equivalent of a packed lunch—no assembly required.
But of course, the devil is in the details. Go delivers on this promise most of the time, but sometimes it decides to be sociable and link to a few system libraries, which is the opposite of what we want. Let’s get our hands dirty and make sure we get what we came for: a truly static binary.
The Default Behavior: Mostly Static, But…
By default, the Go toolchain tries to be smart. On your standard Linux system, it will produce a statically linked binary for most things, but it will dynamically link to a few key system libraries where it might make more sense. The most common culprits are libc (the standard C library) and sometimes libraries for name resolution (net) or user management (os/user).
Why would it do this? On some systems, using the host’s libc can be beneficial for things like DNS resolution that might rely on system-specific configuration (looking at you, /etc/nsswitch.conf). But for deployment, we usually don’t want that. We want our binary to be a hermit, completely unaware of the system it’s running on.
Check if your binary is static using ldd. If it says “not a dynamic executable,” you’ve won. If it lists a bunch of .so files, it’s dynamically linked.
$ go build -o myapp
$ ldd myapp
linux-vdso.so.1 (0x00007ffdcc7f6000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fad8d200000)
/lib64/ld-linux-x86-64.so.2 (0x00007fad8d3f4000)
Ugh. Look at that. It’s making friends. Let’s put a stop to that.
Forcing a Truly Static Binary
To tell the Go linker to sever all these pesky social ties, we need to pass it two specific flags. We do this using the -ldflags (linker flags) option to go build.
$ go build -o myapp -ldflags="-extldflags=-static"
Wait, what? -extldflags passes flags to the external linker (like gcc or clang that Go uses under the hood), and -static tells that linker to avoid dynamic libraries. It’s a bit of a mouthful, but it makes sense once you unravel it.
Now, run ldd again:
$ ldd myapp
not a dynamic executable
Beautiful. Music to my ears. This binary will now run on any Linux machine with a compatible kernel, regardless of what userspace libraries are—or more importantly, are not—installed.
The CGO Dilemma
Ah, but here’s the rub. This magic trick only works if your code doesn’t use cgo. cgo is your gateway to calling C code from Go, and by its very nature, it often requires linking to external C libraries. If you’re using cgo, trying to build a fully static binary will likely result in a linker throwing a tantrum with errors about missing symbols.
The solution? The nuclear option: disable cgo entirely. This tells the Go toolchain to stick to its pure-Go libraries, even for things that might have a cgo-based implementation.
$ CGO_ENABLED=0 go build -o myapp -ldflags="-extldflags=-static"
Setting CGO_ENABLED=0 is the best way to guarantee a static binary. It’s like putting the toolchain in a straightjacket—it can’t possibly link to anything external, even if it wanted to. This is the gold standard for deployment binaries. You should almost always use this.
Cross-Compiling for Fun and Profit
This is where the static binary story gets truly absurd (in the best way). Because Go has its own compiler toolchain and doesn’t rely on the system’s linker, cross-compilation is a first-class citizen. You can build a binary for a completely different operating system and architecture from your laptop.
The formula is simple: set the GOOS (target operating system) and GOARCH (target architecture) environment variables. And remember, we want CGO_ENABLED=0 to ensure it’s static.
Want to build a Linux binary on your Mac? No problem.
$ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64
Need to deploy to a Raspberry Pi? Easy.
$ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o myapp-linux-armv7
You can now develop on your comfortable macOS or Windows machine and produce a lean, mean, static binary that runs on a minimal Linux container or server. It’s not magic; it’s just good engineering. And it’s one of the main reasons Go has become the language of choice for modern command-line tools and cloud-native services. You get one file, you throw it at a container, and you’re done. No fuss.