When Go binaries link against libc
Go is known for producing statically linked binaries, but that is not always the default. On Unix systems, certain standard library packages—notably net for DNS resolution and os/user for user and group lookups—delegate to the C library when cgo is enabled. Since cgo is typically enabled by default on Unix, a program that uses these features can end up dynamically linked even though your code contains no C at all.
For a plain "hello, world" program, the result is fully static. Tools like file, ldd, and nm confirm there are no shared object dependencies and no undefined symbols waiting for a dynamic linker.
The picture changes once you exercise net or os/user. A trivial DNS lookup program built with default settings produces a dynamically linked binary because the standard library routes the lookup through libc when cgo is available. The Go team maintains pure Go implementations of both DNS and user/group lookups, and the toolchain provides two ways to force their use:
- Build with
-tags netgofor thenetpackage. - Build with
-tags osusergofor theos/userpackage. - Or set
CGO_ENABLED=0to disable cgo entirely, which automatically selects the pure Go paths for all standard library packages.
Bringing in real C code
Once your program—or one of its dependencies—imports a package that uses cgo, disabling cgo is no longer an option. A common offender is the go-sqlite3 driver, which requires cgo. Even trivial C code, such as a call to printf wrapped with cgo, results in a dynamically linked binary. The C code calls into libc, and the standard libc implementation on Linux—glibc—is meant to be linked dynamically.
There is a workaround. Instead of linking against glibc, you can use a different C runtime that supports static linking. The musl library is a well-established alternative designed for static builds. After installing musl, point Go at its gcc wrapper:
CC="path/to/musl-gcc" go build -ldflags '-linkmode external -extldflags "-static"'
The CC environment variable tells cgo which C compiler to use. The linker flags force an external linker and ask it to produce a static executable. This approach works not only for direct cgo code but also for larger projects. The repository accompanying the source article includes a program using go-sqlite3; building it normally yields a dynamic binary, while the musl flags yield a fully static one.
An interesting side effect: building a DNS lookup program with musl and without -tags netgo also produces a static binary. The standard library still calls into libc for resolution, but that libc is statically linked musl, so the final executable has no runtime dependencies.
Zig as an alternative
Another approach is to use the Zig toolchain. Zig bundles its own C compiler, linker, and libc for static linking, so it can serve the same role as musl without a separate installation. Point CC at the Zig binary with the appropriate target:
CC="zig cc -target x86_64-linux-musl" go build
Zig's driver sets the required linker flags automatically, making the command line shorter than the musl one. It also supports cross-compilation of Go programs that include C code. However, not everything works flawlessly; attempting to build the same DNS lookup sample with Zig fails with linker errors.
Current state and outlook
Producing a statically linked Go binary on Linux takes some additional steps once cgo is in play, but the paths above work in practice. There is a long-standing accepted proposal to add a -static flag to go build that would hide all of this configuration, but it has not yet been implemented.



