skip to content

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%

answer

  1. the flag decides the artifact's runtime contract
  2. portability against fidelity to the host's lookups
  3. default plus an exception with an owner
  4. enforce on the binary, not the environment variable
  5. verify lookups before the flag ships

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.

solid answer

~50 s

I would set `CGO_ENABLED=0` as the default for shipped artifacts and enforce it on the artifact rather than the environment — a pipeline step that runs `file` on the binary and fails a release that produced a dynamic executable. That default is not free, so it comes with a verification: disabling cgo changes hostname and user resolution to Go's implementations, which do not cover every source the host C library can reach, so a service that depends on one must be identified before the flag is flipped, not after. For the team whose dependency needs cgo, the answer is an exception with a cost attached: a build environment pinned to the target's C library, that service loses the free portable artifact, and someone owns the version coupling. What I would not accept is the flag being quietly flipped per repository, because then nobody knows which artifacts are portable and which are not.

go deeper

for a junior

Know what the flag does before reasoning about policy: it decides whether the shipped binary depends on a C library being present on the host at a compatible version.

for a middle

Be able to describe what a team loses either way — a portable artifact on one side, host-configured hostname and user lookups on the other — and how you would test which one a given service needs.

for a senior

Argue for a default and say how you would verify it per service before it ships, including the A/B of the same commit built both ways against a production-like environment.

for a principal

Own the whole shape: a default, enforcement on the artifact, an exception path with a named owner and attached costs, and a stated condition under which the default itself would be wrong for your fleet.

## What is actually being decided The flag looks like a build detail; the decision is about **what a shipped artifact is allowed to depend on**. `CGO_ENABLED=0` says: a binary plus a compatible kernel is the whole runtime contract. `CGO_ENABLED=1` says: the binary also needs a C library of a compatible version present on every host it will ever run on. That is an operational commitment, and it is owned by whoever owns the deployment artifact — not by whoever added the dependency. This is why it is a decision that can be overruled. A platform team can prefer portability; a feature team can have a genuine need for a C-backed library. Both positions are reasonable, and the job is to make the trade explicit rather than to win it. ## The default, and why Default to `CGO_ENABLED=0` for anything shipped. The arguments: - **The artifact stops being coupled to the builder.** The most expensive failures in this area are environment-shaped: they pass every test in the build environment and appear the first time the binary meets a host with a different C library, in production, in front of the person on call. - **Behaviour becomes a property of the build.** With cgo enabled, which resolver code runs can depend on the host's configuration. With it disabled, the same code runs everywhere, which makes environments comparable. - **Nothing has to be remembered.** A default that must be re-applied per repository is not a default. ## The cost you must not gloss over Disabling cgo changes behaviour, not just packaging. Go's own implementations of hostname and user lookup handle the ordinary sources but cannot load the C library's pluggable name-service modules. If a service resolves internal names that only exist behind such a module, or looks up users from a directory rather than the local files, turning cgo off breaks it — quietly, as a lookup that finds nothing. So the policy is not "set the variable". It is: for each service, establish whether it depends on host-side resolution, then flip the flag, then verify. The cheapest verification is an A/B: build the same commit both ways and run the real lookups against a production-like environment. That evidence is what turns an argument into a decision. ## The exception process For the team whose dependency needs cgo, grant an exception and attach its costs, all of which are real and none of which are catastrophic: - The build environment is now pinned to the C library of the target host, and both move together. Someone owns that pairing and reviews it when either side changes. - That service's artifact is no longer freely portable; a decision to move it to a different host family becomes a project rather than a config change. - The service opts into extra verification at release: proving the artifact starts on a target-like host is now part of its pipeline, because nothing else will catch a mismatch. And ask the question the exception exists to force: **what does the C dependency actually buy?** Often it is one feature, sometimes it has a pure-Go alternative, and occasionally the C work can be moved out of the service that must ship as one file. Those are the conversations worth having; "it is what the library we picked uses" is not an answer, it is a starting point. ## Enforcement that survives contact Enforce on the **artifact**, not the environment. A CI step that inspects the built binary and fails the release if it is a dynamic executable, unless the service carries a recorded exception, is robust in a way that asserting an environment variable is not: a new dependency can force external linking without anyone touching a build script, and the artifact check notices while the environment check does not. Keep the exception list small, visible and dated. Its length is a useful metric on its own — a list that grows means the default is wrong for your fleet, and that is worth knowing. ## When you would change your mind If most services genuinely need host-side resolution — a fleet where internal names come from a directory service and the C library is the only thing that can see them — then defaulting to `CGO_ENABLED=0` fights the environment, and you should default the other way and standardise the build environment against the target C library instead. The right default is the one that matches how your hosts actually resolve names, and it is worth re-asking when the platform changes. ## What a strong answer sounds like A default, an enforcement point that inspects the artifact, an exception path with a named owner and an attached cost, a verification step that covers the behaviour change rather than only the packaging, and a stated condition under which the default itself is wrong.

  • How would you enforce the default so that it cannot drift?
    Check the artifact. A release step that inspects the built binary and fails if it is dynamically linked, unless the service is on a recorded exception list, catches the case that matters: a newly added dependency that forces external linking without anyone editing a build script. Asserting the environment variable was exported does not.
  • What evidence would change your mind about the default for a particular service?
    Proof that it resolves names or users the pure-Go implementations cannot see — an internal hostname served only by a name-service module, or accounts that live in a directory rather than the local files. An A/B run of the same commit built both ways against a production-like environment produces that evidence in an afternoon.
  • The team says the C dependency is non-negotiable. What do you ask next?
    What it provides, whether a pure-Go alternative covers it, and whether that work can live outside the artifact that must ship as one file. If none of those hold, grant the exception and attach the costs: a build environment pinned to the target C library, a named owner for that pairing, and a start-up check against a target-like host in their pipeline.

saying these in an interview costs you the question

  • Treats it as a build-script preference rather than a runtime contract
  • Mandates the flag without checking what resolution each service needs
  • Enforces by asserting an environment variable instead of inspecting the artifact
  • Grants exceptions with no owner, no cost and no expiry
  • Refuses the exception outright and blocks a legitimate feature