Why can a Go binary built with cgo enabled fail to start on a host with a different libc?
answer
- the binary is less self-contained than it looks
- you never wrote import "C", but net did
- the loader runs before your main does
- glibc on the build box, musl on the host
- a missing interpreter is reported as a missing file
basics
~20 sWith cgo enabled, standard packages such as net and os/user call into the host C library, so the binary is dynamically linked against libc. It needs a compatible libc present at run time, or the loader refuses to start it.
solid answer
~50 sA Go build with cgo enabled is not automatically self-contained. You may never write `import "C"` yourself, but on Linux the `net` and `os/user` packages have C-backed lookup paths, and linking those in makes the output a dynamically linked ELF binary that records an interpreter and needs the C library at exec time. Copy that binary onto a host built around a different C library, or onto a filesystem with no C library at all, and the kernel fails the exec before your `main` runs — usually with the confusing message `no such file or directory`, which is about the missing loader, not the binary. Build against a newer glibc and run on an older one and you instead get a `GLIBC_2.xx not found` symbol-version error. Building with cgo disabled removes the dependency: the pure-Go implementations are linked in instead and the Linux binary is statically linked.
code
text · 10 lines$ ./peer-sync
listening on :8080
# copied to a host with a different C library
$ ./peer-sync
-bash: ./peer-sync: No such file or directory
$ file ./peer-sync
./peer-sync: ELF 64-bit LSB executable, x86-64, dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2go deeper
Be ready to say that cgo can be pulled in by the standard library rather than by your own code, and that the result needs a matching C library on the host. Recognising the misleading "no such file or directory" symptom is the point of the question.
Explain the mechanics: a dynamically linked ELF binary names an interpreter and needed libraries, the loader resolves them before main runs, and glibc symbol versioning is backward but not forward compatible.
Show that you check the artefact instead of arguing about the build script, and that you can name what a cgo-free rebuild changes behaviourally rather than treating it as a free win.
Own this as a release-policy question: which libc your artefacts may depend on, who is allowed to make an exception, and what the build pipeline enforces so the answer does not vary per developer laptop.
## The claim that "Go produces static binaries" is conditional Go compiles your code plus the whole Go runtime into one executable, and on Linux it issues system calls directly rather than through the C library. That is why the "single static binary" reputation exists, and it is accurate — **as long as nothing in the build pulls in cgo**. cgo is the mechanism that lets Go code call C. It is on by default for a native build when a C compiler is available. The catch is that you do not have to use it deliberately to get it: a few standard-library packages ship **two implementations** of the same lookup, one written in Go and one that goes through the platform's C library, and they choose between them at build time based on whether cgo is available. The two that matter on Linux are: - **`net`** — hostname and address resolution. The C-backed path calls the system resolver, which is what a normal C program uses and what honours the host's name-service configuration. - **`os/user`** — account and group lookups. The C-backed path calls the system's account functions, which is how a C program sees users that are not in a local file. If either of those C-backed paths is linked in, the output is no longer a self-contained executable. It becomes a **dynamically linked ELF binary**: it records the path of a dynamic loader (the "ELF interpreter") and a list of shared libraries it needs, the C library among them. ## What happens at exec time When you run a dynamically linked binary, the kernel does not jump straight into your code. It loads the interpreter named inside the binary, and that loader maps the shared libraries the binary asked for. Three things can go wrong, and they produce three different symptoms: 1. **No C library at all.** On a minimal root filesystem that contains only your binary, there is no loader at the recorded path. The kernel's exec fails with `ENOENT`, which the shell prints as `no such file or directory` — pointing at a file that visibly exists. This is the single most confusing symptom in this whole area, and recognising it is most of the answer. 2. **A different C library implementation.** glibc and musl are not drop-in compatible and do not use the same loader path. A binary linked against one will not start on a host that only has the other, again with the misleading `ENOENT`. 3. **The same implementation, an older version.** glibc uses symbol versioning and is backward compatible, not forward compatible. Build on a newer distribution and run on an older one and the loader reports something like `version 'GLIBC_2.xx' not found`. The usual fix is to build against the oldest C library you must support — or to stop linking one. ## What disabling cgo changes With cgo disabled, the toolchain links the **pure-Go** implementations instead: Go's own DNS resolver (the netgo path) parses the host's resolver configuration and hosts file and speaks DNS itself, and `os/user` parses the local account files directly. Nothing in the program calls into a C library, so on Linux the result is a statically linked executable that starts on any host of the right architecture, whatever userland it has. That is a real behavioural swap, not just a linking change — the pure-Go implementations do not consult the host's name-service switch configuration or its pluggable name-service modules, so names and accounts that only those modules can provide stop resolving. Portability is bought with fidelity to the host's configuration. ## Platform caveats "Disable cgo, get a static binary" is a Linux statement. On macOS the runtime must go through the system library to make syscalls, so a native macOS build is always dynamically linked against system libraries regardless of what you set; you cannot ship a fully static macOS binary. Windows likewise links system DLLs. ## How you check You do not have to guess from the source. `file` on the binary says `statically linked` or `dynamically linked, interpreter …`; `ldd` prints the shared libraries or says the file is not a dynamic executable; and `go version -m` on the binary prints the build settings that produced it, including whether cgo was enabled. Those three answers, taken from the artefact rather than from the build script someone believes ran, settle the question in under a minute.
- The binary is right there and executable, so why does the shell say "no such file or directory"?The message is about the dynamic loader, not the binary. A dynamically linked ELF file records the path of its interpreter; if that path does not exist on the host, the kernel's exec fails with ENOENT and the shell reports it against the command you typed. The binary itself was found and read successfully.
- You build on a newer distribution and the host reports a GLIBC version not found. What is that, and how do you avoid it?glibc versions its symbols. Linking on a newer glibc records symbol versions that an older glibc does not export, and glibc is backward compatible but not forward compatible. Either build in an environment with the oldest C library you must support, or build without cgo so nothing references those symbols at all.
- Does building with cgo disabled always give you a fully static binary?On Linux, yes for the ordinary case: the pure-Go lookups are linked in and nothing needs a shared C library. It is not universal, though. On macOS the Go runtime must go through the system library to reach the kernel, so a native macOS binary is always dynamically linked against system libraries no matter how you set the flag.
It is like shipping a document that quietly references a font installed on your laptop. The file opens fine at your desk and fails on a machine that never had the font.
saying these in an interview costs you the question
- Claims every Go binary is statically linked by definition
- Thinks cgo only matters if you write import "C" yourself
- Blames a corrupt or truncated file when the loader reports ENOENT
- Treats glibc and musl as drop-in compatible
- Says the Go runtime must be installed on the target host