skip to content

How do you decide whether your team's Go services ship binaries that link libc at all?

level: principalimportance: nice to knowfreq 22%

answer

  1. a default plus a recorded exception
  2. two owners, one artefact
  3. name the behaviour you are giving up
  4. assert it on the binary, not the environment
  5. canary on a host that uses the directory service

basics

~20 s

Make it a release policy, not a per-build flag. Default to cgo-free binaries for portability, require a documented exception where a service truly needs lookups only the host C library can perform, and agree it with whoever owns the hosts.

solid answer

~50 s

I treat it as a platform default with an explicit exception path. Cgo-free is the default because it decouples the artefact from the host: one binary runs on any userland of the right architecture, and the resolver you tested is the resolver that runs. The cost is real and I name it rather than hide it — Go's own name and account lookups ignore the host's name-service switch and its loadable modules, so environments whose naming or accounts come from a directory service see a behavioural change, not just a linking change. So the decision is owned jointly: the release owner sets the default and enforces it on the artefact, and the team owning the runtime environment can overrule it for a service whose lookups genuinely depend on host configuration. Any service that keeps cgo gets its build pinned to the oldest C library in the fleet.

go deeper

for a junior

Know that this is a deliberate choice somebody owns, and that a binary linking the host C library is tied to hosts that carry a compatible one. You are not expected to set the policy yet.

for a middle

Be able to state both sides concretely: portability and reproducible resolution against losing the host's name-service switch and directory-provisioned accounts. Avoid presenting either option as free.

for a senior

Show how you would verify the choice — a canary on a host that really uses those name sources, a check that reads the artefact's recorded build settings — rather than trusting that the pipeline set the flag.

for a principal

Own the policy: a default enforced in the pipeline, a documented exception path with named owners and expiry, and an explicit agreement with the team operating the runtime environment about who may overrule it.

## Why this is a decision and not a preference Whether your binaries link the host C library looks like a build flag, but it determines three things that different people own: what an artefact can run on, how a class of lookups behaves in production, and who is on the hook when the two disagree. That is the shape of a policy question. ## The case for cgo-free as the default - **One artefact, any host.** No dependency on which C library the runtime environment ships or which version of it. The most common outage in this area — a binary built on a newer build image failing on an older host — simply cannot happen. - **Behavioural uniformity.** The resolver and the account parsing are the same code in CI, in staging and in production. When resolution comes from the host's C library, the behaviour is a property of the host, and hosts drift. - **Simpler builds.** Cross-compiling to another target needs no C toolchain for that target, and the build has one fewer external input to reproduce. - **Smaller supply-chain surface.** The C library and any name-service modules it loads are code running inside your process that your dependency review never looked at. ## The case against, stated honestly A cgo-free build is not the same program statically linked; it is a program that sees a smaller world. - The host's name-service switch ordering is ignored, and loadable name-service modules — a local mDNS responder, a container platform's own name source, a directory-service client — are never consulted. - Accounts provisioned only in a directory service are invisible to `os/user`, which parses local files. - Some environments genuinely encode real policy in that configuration, and telling them "your naming is wrong" is not a plan. - The default is Linux-shaped. Native macOS builds link system libraries regardless, so a claim of "all our binaries are static" is false on developer machines and in any macOS runner. ## How I would run the decision **Pick a default and enforce it mechanically.** Cgo off in the release pipeline, asserted on the artefact rather than trusted from the environment: the build settings recorded in the binary are machine-readable, so a release check can fail when they do not match policy. A default that lives in a wiki page is not a default. **Make the exception legible.** A service that needs the C-backed lookups files an exception that records *why*, *which lookups*, and *what runtime environment the artefact now requires*. Reviewing it is the same conversation as reviewing a new dependency, because that is what it is. **Give the runtime owner a veto.** The team operating the hosts knows whether names and accounts really come from name-service modules. They can overrule the default for their environment; what they cannot do is leave it undecided, because the failure lands on them at 3am in the least legible possible form — a process that exits before it logs anything, or one that resolves most names and quietly fails a subset. **Verify behaviourally, not just structurally.** "It builds and starts" is not evidence. The test is a canary on a host that actually uses the directory service and the local name sources, exercising the lookups the service depends on. **Pin the build side of any exception.** If a service keeps cgo, the build environment is pinned to the oldest C library in the supported fleet, because symbol versioning is backward compatible and not forward compatible. That pin has an owner and an expiry review, or it rots. ## The tradeoffs a lead should be able to price - *Portability versus fidelity.* You are trading exact agreement with host configuration for an artefact that does not care about the host. Most services never notice; a few are defined by it. - *Uniformity versus escape hatches.* A single policy is cheap to operate and occasionally wrong. Per-service choice is always right and expensive to keep honest. The compromise — a strict default plus a recorded exception — is usually the best available. - *Where the fix lives.* Frequently the cheapest resolution is neither flag: ship the one hosts entry or account record the service needs into the runtime filesystem, or query the authoritative source directly, and keep the portable artefact. - *Who carries the risk.* A cgo-free default moves risk from "the host might not match the build" to "the program might not see everything the host can". Both are real; the second is at least uniform across your fleet and testable in CI. ## What a weak answer looks like Declaring cgo-free universally correct without naming what it removes, or treating it as a developer preference expressed in a Makefile. Both produce the same outcome: a policy nobody agreed to, discovered during an incident.

  • What would make you accept a C-linked binary for one particular service?
    A lookup it genuinely cannot do otherwise — hostnames or accounts supplied by the host's name-service modules — or a dependency wrapping a C library with no pure-Go equivalent. Then I pin the build environment to the oldest C library in the fleet, record the runtime requirement as part of the release contract, and give the exception a review date rather than letting it become permanent by default.
  • How do you stop that exception from spreading across the estate?
    Default the pipeline to cgo off and make the release check read the artefact's recorded build settings, failing anything that links the C library without a registered exception. Review each exception like a dependency decision, with a named owner and an expiry. Enforcement on the artefact is what keeps it from drifting back through a copied build script.
  • What do you owe the team that operates the hosts before flipping the default?
    A written statement of which lookups change behaviour, a canary on a host that actually uses their directory service and local name sources, and a rollback that is a rebuild rather than a redesign. They own the environment whose configuration you are choosing to ignore, so they get a veto and enough information to use it.
  • Someone proposes keeping the C-backed lookups but linking the C library statically. Is that a good compromise?
    Usually not with glibc. Its name-service switch loads modules as shared objects at run time, so the statically linked binary still needs matching shared libraries present to perform exactly the lookups you kept cgo for. You get the portability problem back in a form that is harder to detect, which is worse than either clean option.

saying these in an interview costs you the question

  • Declares cgo-free always correct without naming what it loses
  • Treats it as a per-developer build flag rather than release policy
  • Ignores that host name and account resolution behaviour changes
  • Promises fully static binaries on macOS as well
  • Decides unilaterally without the team that owns the runtime hosts
  • Verifies only that the service starts, never the lookups it depends on