33.7 Pure Go Builds: CGO_ENABLED=0
Let’s talk about getting rid of C. I know, it sounds blasphemous, but sometimes you just want a clean, simple, dependency-free Go binary. No linking against libc, no worrying about cross-compilation toolchains, no fuss. That’s where CGO_ENABLED=0 comes in—your ticket to a pure Go build.
You see, the Go toolchain is a bit of a split personality. By default, it’s friendly with C. It uses CGo to bridge the gap between Go and the vast, ancient world of C libraries. This is fantastic when you need to talk to a hardware SDK or a battle-tested library like SQLite. But this friendship comes at a cost: your binary is now tied to the C library on the target machine (usually libc), and cross-compiling becomes a nightmare of installing obscure cross-compiler toolchains.
Setting CGO_ENABLED=0 tells the Go toolchain to ignore this whole C world. It switches the compiler into a pure Go mode. The most immediate and beautiful consequence? You can cross-compile from your cozy macOS laptop to a Linux ARM64 server with ridiculous ease. No need for gcc-arm-linux-gnu or any of that nonsense. It just works.
The Magic Incantation
You set it as an environment variable right before your go build command. It doesn’t matter what your CC environment variable is set to; when CGO_ENABLED=0, the Go toolchain simply won’t call an external C compiler.
# Build a pure Go, statically linked binary for Linux from anywhere
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o my-app .
The -o my-app binary you get from this is a beautiful, statically linked executable. It has no external dependencies. You can scp it to a vanilla Linux container running on a Raspberry Pi, make it executable with chmod +x, and run it. No apt-get install needed. It’s glorious.
What Actually Breaks?
This is the crucial part. When you set CGO_ENABLED=0, you’re not just disabling your own CGo code; you’re disabling all CGo code, including the sneaky bits hidden in the standard library.
On most Unix-like systems (Linux, BSD, macOS), the net and os/user packages have two implementations: a pure Go one and a CGo one. The CGo one is often the default because it allows for system-level name resolution (e.g., respecting /etc/nsswitch.conf) and fetching user information without parsing /etc/passwd (which can be a problem if you’re in a Docker container without those files).
When you set CGO_ENABLED=0, you force these packages to use their pure Go fallbacks. Let’s see what that looks like:
package main
import (
"fmt"
"os/user"
)
func main() {
currentUser, err := user.Current()
if err != nil {
panic(err)
}
fmt.Printf("Hello, %s (UID: %s)\n", currentUser.Username, currentUser.Uid)
}
Build this normally on your Mac, and it’ll use the CGo implementation. Now build it with CGO_ENABLED=0. It will seamlessly switch to the pure Go version, which, and this is the key gotcha, can have different behavior. The pure Go user.Current() relies on environment variables ($USER, $HOME) and can fail if they’re not set properly, whereas the C version asks the system directly.
The net package behaves similarly. The pure Go resolver might not respect all the esoteric system configuration that libc’s getaddrinfo does. For 99% of use cases, this is fine. For the other 1%, you might pull your hair out wondering why DNS lookups are different inside your minimal Docker image.
The Dockerfile Best Practice
This is where CGO_ENABLED=0 becomes non-negotiable. If you’re using a scratch or alpine base image, there is no libc. A default Go binary will simply not run. You’ll get the infuriating not found error because the dynamic linker doesn’t exist.
# Multi-stage build for a tiny, secure container
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
# Force a static, pure Go build for the target Linux environment
RUN CGO_ENABLED=0 GOOS=linux go build -o /my-app .
# Final stage: use the absolute minimal base image
FROM scratch
COPY --from=builder /my-app /my-app
ENTRYPOINT ["/my-app"]
This produces a container image that is, essentially, just your application and nothing else. No shell, no package manager, no libraries. It’s the ultimate in minimalism and security. You can’t get much more direct than that.
So, use CGO_ENABLED=0 as your default for production builds. It simplifies deployment and eliminates a whole class of “but it works on my machine” issues. Just be smart about it: test your application thoroughly in this mode to ensure the pure Go versions of the standard library packages don’t introduce any surprising behavior. It’s not that the designers made a questionable choice here; they gave you options. It’s on you to know which one to use.