skip to content

Static Linking and CGO_ENABLED

CGO_ENABLED=0 yields a binary with no libc to find at run time, and gives up the cgo resolvers in net and os/user. The canonical 'no such file or directory' interview story.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

What does building with CGO_ENABLED=0 change about a Go binary, and what do you give up?

level: juniorimportance: must knowfreq 70%

answer

  1. a build-time switch, not a runtime one
  2. no C compiler in the picture
  3. two stdlib packages ship two implementations
  4. one file, no dynamic loader
  5. hostname and user lookups change owner

basics

~20 s

CGO_ENABLED=0 disables cgo, so no C code is compiled in and the standard library uses its pure-Go net and os/user code. The result is a self-contained static binary that needs no C library on the host.

solid answer

~40 s

`CGO_ENABLED` is an environment variable that `go build` reads. Setting it to `0` turns cgo off: any file with `import "C"` is excluded by build constraints, and two standard-library packages that ship two implementations each — `net` for hostname lookups and `os/user` for user lookups — fall back to their pure-Go versions. On Linux the practical effect is that the Go linker no longer hands off to the system C toolchain, so you get a statically linked executable with no dynamic loader and no libc dependency. What you give up is real: the pure-Go resolver only understands `files` and `dns` sources, so hostnames or users served by other name-service modules (mDNS, an LDAP-backed directory) stop resolving, and any dependency that binds a C library will not build at all.

code

text · 8 lines
text
$ CGO_ENABLED=0 go build -o app .
$ file ./app
./app: ELF 64-bit LSB executable, x86-64, ... statically linked, ...

$ CGO_ENABLED=1 go build -o app .
$ file ./app
./app: ELF 64-bit LSB executable, x86-64, ... dynamically linked,
       interpreter /lib64/ld-linux-x86-64.so.2, ...

go deeper

for a junior

Be ready to say what the switch does in one breath: no C code, pure-Go net and os/user, one static file with no libc dependency. Knowing it is set at build time, not run time, is the part that gets checked.

for a middle

Explain the mechanics: cgo files are excluded by build constraints, the Go linker links internally instead of handing off to the C toolchain, and the resulting ELF names no interpreter. Be able to show the difference with file or ldd.

for a senior

Show that you know the switch changes behaviour, not just packaging. Name what the pure-Go resolver cannot do and how you would verify a service does not depend on it before flipping the flag in a release pipeline.

for a principal

Own it as a release-artifact policy: what the default is, who is allowed an exception, and what evidence is required to grant one. The tradeoff is portability of the artifact against fidelity to the host's name-service configuration.

## What cgo is, in one paragraph cgo is the mechanism that lets Go code call C code. When a Go file contains `import "C"`, the `go` command runs cgo over it, invokes a C compiler, and links the result into the program. Even if *your* code never imports `C`, cgo matters to you, because parts of the Go standard library are written twice — once in Go and once against the host's C library — and cgo decides which copy you get. ## What the variable actually is `CGO_ENABLED` is an environment variable consulted by `go build`, `go test` and friends. For a native build it defaults to `1` when a usable C toolchain (gcc or clang) is on `PATH`. Setting `CGO_ENABLED=0` on the command line — `CGO_ENABLED=0 go build -o app .` — turns the whole facility off for that build. It is a *build-time* switch. Nothing about it can be changed after the binary exists; you cannot flip it at startup. ## Three consequences **1. Files that import C disappear.** cgo files carry an implicit build constraint, so with cgo off they are simply not part of the package. If a package has no non-cgo implementation of the same symbols, the build fails outright. That is the honest answer to "why won't this dependency compile in our release build?" **2. Two standard-library packages switch implementation.** `net` has a hostname resolver written in Go and a path that calls the C library's `getaddrinfo`; with cgo disabled, only the Go one exists in the binary. `os/user` likewise has a pure-Go implementation that parses `/etc/passwd` and `/etc/group` directly, versus a cgo path that calls the C library's password-database functions. These are the two packages people actually notice. **3. Linking changes.** When cgo is in play, the Go linker uses *external* linking: it hands the object files to the system C toolchain, and the result is an ELF binary that is dynamically linked and names an interpreter — the dynamic loader, typically `/lib64/ld-linux-x86-64.so.2` on 64-bit x86 Linux. With `CGO_ENABLED=0` the Go linker links internally and emits a statically linked executable with no interpreter and no shared-library dependencies. This is why teams that ship a single file into an image containing nothing else standardise on `CGO_ENABLED=0`: the binary plus a kernel is the entire runtime requirement. ## How you check Inspect the artifact, do not assume: - `file ./app` reports either `statically linked` or `dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2`. - `ldd ./app` prints the shared libraries needed, or says the file is not a dynamic executable. One `file` call in the release pipeline catches an accidental cgo dependency long before a host does. ## What you give up, concretely The cgo-backed resolver goes through the host's C library, which means it honours the host's full name-service configuration. Go's pure-Go resolver reads `/etc/resolv.conf` and understands the `files` and `dns` sources; it cannot load the C library's other name-service modules, because those are shared objects opened at run time. So a hostname that only resolves through mDNS, or a username that only exists in a directory service, resolves on a developer laptop and fails inside a `CGO_ENABLED=0` binary. Nothing is logged about the switch — the lookup just does not find the name. The same applies to `os/user`: a user that exists only in a directory is invisible to code that parses `/etc/passwd`. You also give up the ability to use any dependency that wraps a C library. That is a build failure rather than a silent behaviour change, which is the kinder of the two. ## Platform note This whole discussion is a Linux one. On macOS, Apple does not support statically linking the system library, so "a fully static binary" is not a goal you can reach there; on Windows the packaging story is different again. When an interviewer asks about `CGO_ENABLED=0`, they almost always mean a Linux server artifact. ## The short version to say out loud Off means: no C, pure-Go `net` and `os/user`, one static file. The cost is that name and user lookups now follow Go's rules rather than the host's, and C-backed dependencies stop building.

  • Which standard-library packages actually behave differently when cgo is disabled?
    `net` and `os/user` are the two that matter. `net` loses the path that calls the C library's `getaddrinfo` and uses its pure-Go hostname resolver instead; `os/user` stops calling the C password-database functions and parses `/etc/passwd` and `/etc/group` itself. Everything else in the standard library is Go either way.
  • If a dependency requires cgo, what does the failure look like?
    A build failure, not a runtime one. With cgo off, files containing `import "C"` are excluded by build constraints, so the package is left with missing symbols or no implementation and `go build` reports undefined references or an empty package. That is preferable to a silent behaviour change — you find out in CI.
  • How do you prove in CI that a released binary is really static?
    Run `file` or `ldd` on the built artifact as a pipeline step and fail the release if it reports a dynamic executable or lists shared libraries. Asserting the environment variable was set is weaker: someone can add a dependency that forces external linking without touching the build script.

A cgo-enabled binary is like a recipe that says "use the kitchen's stock": it depends on what the host has installed. CGO_ENABLED=0 packs its own ingredients — heavier to assemble, but it cooks anywhere.

saying these in an interview costs you the question

  • Thinks CGO_ENABLED can be changed at run time
  • Believes a CGO_ENABLED=0 binary embeds a copy of glibc
  • Assumes disabling cgo changes nothing about hostname resolution
  • Claims the Go toolchain ships its own C compiler
  • Says a static binary must be rebuilt for each kernel version
open as a page

A Go binary built on a glibc host dies on a musl host with "no such file or directory" — why?

level: seniorimportance: must knowfreq 55%

basics

~20 s

The binary was linked with cgo enabled, so it is dynamically linked and names a glibc dynamic loader such as /lib64/ld-linux-x86-64.so.2. That path does not exist on a musl host, so exec fails and reports the missing interpreter.

open as a page

What do the netgo and osusergo build tags do when cgo stays enabled?

level: middleimportance: should knowfreq 42%

basics

~20 s

Both are build tags that force a pure-Go implementation even with cgo enabled: netgo pins the net package to its Go hostname resolver, osusergo pins os/user to the version parsing /etc/passwd. Neither makes the binary static.

open as a page

How do you decide whether CGO_ENABLED=0 is mandatory for every shipped binary when one team's dependency needs cgo?

level: principalimportance: should knowfreq 32%

basics

~20 s

Default to CGO_ENABLED=0, because it makes the artifact independent of the host C library, then run an explicit exception process for services that need cgo: a named owner, a build environment pinned to the target libc, and verified lookups.

open as a page

What does building with -ldflags '-extldflags "-static"' do, and why does glibc warn about getaddrinfo?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

It forwards -static to the external C linker, so a cgo build links the C library into the executable instead of loading it at startup. glibc still opens its lookup modules at run time, so it warns.

open as a page