skip to content

When does Go's net package use its pure-Go DNS resolver instead of the cgo one?

level: middleimportance: must knowfreq 40%

answer

  1. two implementations behind one API
  2. one costs a goroutine, the other a thread
  3. Go steps aside for host features
  4. an environment variable flips it
  5. PreferGo is a preference on one resolver value

basics

~20 s

Go has two name resolvers: its own DNS client and one that calls the C library. The Go one is the default on Unix; Go falls back to C when the platform forbids direct queries or the host's resolver configuration needs features Go lacks.

solid answer

~40 s

The `net` package can resolve names two ways: with Go code that talks DNS itself, or by calling into the system's C resolver through cgo. The Go resolver is preferred by default because a blocked lookup costs only a parked goroutine, while a blocked C call ties up an operating system thread. Go switches to the cgo path when it cannot do the job faithfully: on systems that do not permit programs to make direct DNS queries, when `/etc/resolv.conf` or `/etc/nsswitch.conf` request features the Go resolver does not implement, when environment variables such as `LOCALDOMAIN`, `RES_OPTIONS` or `HOSTALIASES` are set, and for `.local` mDNS names. You can force the choice: `GODEBUG=netdns=go` or `netdns=cgo` at run time, the `netgo` or `netcgo` build tags at build time, or `net.Resolver{PreferGo: true}` for one resolver value in code.

code

go · 7 lines
go
r := &net.Resolver{PreferGo: true}

addrs, err := r.LookupHost(ctx, "api.internal")
if err != nil {
	return err
}
use(addrs)

go deeper

for a junior

Know that Go can resolve names either by speaking DNS itself or by calling the system's C resolver, and that the same code can therefore behave differently on different machines.

for a middle

Be ready to explain the goroutine-versus-thread cost that makes the Go resolver the default, and to name at least two conditions that push resolution back onto the C path.

for a senior

Demonstrate that you check which resolver ran before theorising, and that you know the three levers — environment variable, build tag, PreferGo — and which of them you can change without a rebuild.

for a principal

Own the consistency argument: whether every service in the fleet resolves the same way in development, CI and production, and what the platform's base image is allowed to change underneath you.

## Two resolvers, one API Everything in `net` that turns a name into an address — `LookupHost`, `LookupIP`, and the dial path that `net.Dial` uses — sits on top of one of two implementations, chosen at run time: - **The Go resolver.** Go builds DNS queries itself, sends them to the nameservers listed in `/etc/resolv.conf`, and parses the responses. No C code involved. - **The cgo resolver.** Go calls the platform's C name-resolution routines, which run the host's full name-service stack: the hosts file, DNS, and whatever else the system's configuration plugs in. The API you call is identical. Which one runs underneath changes behaviour in ways that show up only when the environment changes. ## Why the Go one is the default The reason is concurrency cost. A DNS query that hangs for five seconds in the Go resolver parks a goroutine — a few kilobytes of stack and no operating-system resource. The same query through the C library blocks a thread, because the C call is opaque to Go's scheduler and the runtime must give it a thread to sit on. A service doing thousands of concurrent lookups against a slow nameserver will spawn thousands of threads under the cgo resolver and simply queue goroutines under the Go one. There is a secondary reason: a binary that never needs cgo for name resolution is much easier to build and ship, because it does not depend on the C library present on the machine that runs it. ## When Go gives up and calls C Go uses the cgo path when it cannot faithfully reproduce what the system would do. The documented triggers include: - **The platform does not let programs make DNS requests directly.** macOS is the usual example, and Windows resolves through system APIs rather than Go's DNS client. A developer laptop therefore very often is *not* using the Go resolver, while the Linux container running the same code is. - **`/etc/resolv.conf` or `/etc/nsswitch.conf` asks for something Go does not implement.** The name-service switch can route lookups through mechanisms far beyond DNS. If the configuration names one the Go resolver does not support, Go steps aside rather than quietly resolving differently. - **Certain environment variables are set** — `LOCALDOMAIN` (even empty), a non-empty `RES_OPTIONS`, a non-empty `HOSTALIASES`. These are C-resolver knobs, so their presence forces the C path. - **mDNS-style names**, those ending in `.local`, which Go's DNS client does not handle. And of course, if the binary was built without cgo support at all, there is no C resolver to fall back to: the Go resolver handles everything. ## Forcing the choice Three levers, with different blast radius: 1. **Run time, per process:** `GODEBUG=netdns=go` or `GODEBUG=netdns=cgo` in the environment. Nothing is rebuilt; you can flip it on a deployment and roll it back. Adding `+1` or `+2` also turns on debug printing, so `netdns=go+2` both forces the Go resolver and shows you what it decided. 2. **Build time, per binary:** the `netgo` build tag forces the Go resolver into the artifact; `netcgo` forces the other. This is baked in and cannot be changed by whoever runs the binary. 3. **In code, per resolver value:** `&net.Resolver{PreferGo: true}` asks that this resolver use Go's built-in DNS client where the platform supports it. That is a library-local decision — it affects lookups made through that resolver and nothing else. `PreferGo` is a preference, not a guarantee for every platform, and it does not change the rest of the semantics: the Go resolver still consults the hosts file, and still follows the lookup order it derived from the system configuration. ## What actually changes when the resolver changes The features the C stack provides and Go does not are the whole story: anything the name-service switch routes somewhere other than DNS, mDNS names, and the C-only environment knobs. Caching also changes hands — the host may cache answers for the C path, and nothing caches them for the Go path. The practical consequence is that the same binary can resolve a name on one machine and fail on another for reasons that have nothing to do with your code, which is why the first diagnostic step for any DNS oddity in a Go service is to establish which resolver actually ran.

  • Why does the net package prefer its own resolver rather than the C library's by default?
    Cost under concurrency. A lookup blocked in Go's resolver holds only a goroutine, so ten thousand slow lookups cost ten thousand cheap stacks. A lookup blocked inside a C call holds an operating-system thread, because the runtime cannot reschedule around opaque C code — so the same load spawns threads. The Go path also removes a build-time dependency on the host's C library.
  • Does the pure-Go resolver still read /etc/hosts?
    Yes. Go's resolver derives a lookup order from the system configuration and consults the hosts file as part of it, so an entry there still wins in the usual `files` then `dns` ordering. Choosing the Go resolver does not turn the hosts file off; what it drops is the name-service mechanisms beyond files and DNS that the C stack can be configured to use.
  • Which lever would you reach for to change the resolver on a service that is already deployed?
    The environment variable — `GODEBUG=netdns=go` (or `cgo`) — because it needs no rebuild and can be reverted by rolling back the deployment's environment. Build tags such as `netgo` and a `PreferGo` resolver in code both bake the decision into the artifact, which is fine as a considered policy but useless during an incident.

saying these in an interview costs you the question

  • Believes Go always uses the C library to resolve names
  • Thinks the resolver choice is fixed at compile time only
  • Says PreferGo disables the hosts file
  • Cannot name a single condition that forces the cgo path
  • Assumes a developer laptop and a Linux container resolve identically