What does net.LookupHost return, and how do you tell a nonexistent hostname from a resolver failure?
answer
- addresses come back as strings
- a missing name is an error
- one error type carries three booleans
- errors.As, then read IsNotFound
- never grep the error text
basics
~20 snet.LookupHost takes a hostname and returns a slice of IP address strings plus an error. To tell a missing name from a broken resolver, unwrap the error to *net.DNSError with errors.As and check its IsNotFound field.
solid answer
~40 s`net.LookupHost(host)` returns `([]string, error)` — the addresses as strings, in whatever order the resolver produced. A name that does not exist is an **error**, not an empty slice: you get a `*net.DNSError` with `Err` set to a message like `no such host`. Never match on that text; instead do `var dnsErr *net.DNSError; errors.As(err, &dnsErr)` and read the booleans: `IsNotFound` means the name genuinely has no records, `IsTimeout` means the query did not get an answer in time, `IsTemporary` means a retry may help. `DNSError` also carries `Name` (what was queried) and `Server` (which nameserver answered), which is what you actually want in a log line. Use `net.DefaultResolver.LookupHost(ctx, host)` when you need the lookup to be cancellable, since the package-level function takes no `context.Context`.
code
go · 9 linesaddrs, err := net.DefaultResolver.LookupHost(ctx, name)
if err != nil {
var dnsErr *net.DNSError
if errors.As(err, &dnsErr) && dnsErr.IsNotFound {
return fmt.Errorf("%s is not registered in DNS: %w", name, err)
}
return fmt.Errorf("lookup of %s failed, retryable: %w", name, err)
}
use(addrs)go deeper
Be ready to state the signature from memory: a hostname in, a slice of address strings and an error out. Know that a name that does not exist produces an error rather than an empty slice.
An interviewer expects you to name *net.DNSError and its IsNotFound, IsTimeout and IsTemporary fields, and to reach them with errors.As so wrapping does not break the check.
Show the classification you build on top: permanent failures alerted rather than retried, transient ones retried with backoff, and Name plus Server in the log line so on-call can see which nameserver answered.
Own the convention across services: one shared classification helper rather than each team inventing its own string match, so retry behaviour on name failures is consistent and reviewable.
## What the function is `net.LookupHost` is the simplest name-resolution entry point in Go's standard library: ```go func LookupHost(host string) (addrs []string, err error) ``` Given a hostname it returns the addresses that name resolves to, each formatted as a string such as `93.184.216.34` or `2606:2800:220:1:248:1893:25c8:1946`. If you hand it something that is already an IP address literal, it returns that literal back unchanged without doing a query. It resolves the way the platform is configured to resolve — typically the hosts file first and then DNS, though that order is itself configurable — so a name in `/etc/hosts` will resolve even with no nameserver reachable. There are two closely related calls worth knowing. `net.LookupIP(host)` returns `[]net.IP` values instead of strings, which saves you re-parsing them. `net.Resolver` has method forms — `LookupHost(ctx, host)`, `LookupIPAddr(ctx, host)`, `LookupNetIP(ctx, network, host)` — and those take a `context.Context`, so they can be cancelled or given a deadline. The package-level functions are thin wrappers over `net.DefaultResolver` with a background context, which means **a package-level lookup cannot be cancelled**. In a server, prefer the resolver method with the request's context. ## The error is the interesting part A name that does not exist does not come back as an empty slice and a nil error. It comes back as an error. Every failure from these calls is a `*net.DNSError`: ```go type DNSError struct { Err string // description of the error Name string // name looked up Server string // server used IsTimeout bool IsTemporary bool IsNotFound bool } ``` The distinction that matters operationally is between *the name does not exist* and *I could not find out whether the name exists*. They call for opposite responses. A nonexistent name is usually a configuration bug — a typo, a service that was never registered, a missing search domain — and retrying it in a loop just burns queries. A timeout or a refused query is an infrastructure condition and retrying is exactly right. `IsNotFound` is the flag for the first case: the resolver got an authoritative answer that there are no addresses for this name. `IsTimeout` means no answer arrived in time. `IsTemporary` is the broader hint that the condition may clear on its own. ## Reaching the fields correctly Use `errors.As`, not a type assertion: ```go var dnsErr *net.DNSError if errors.As(err, &dnsErr) && dnsErr.IsNotFound { // permanent: do not retry } ``` A direct assertion `err.(*net.DNSError)` works only when the `*net.DNSError` is the outermost error. The moment the lookup happens somewhere deeper — inside a dial, inside a client call, inside your own wrapping with `fmt.Errorf("...: %w", err)` — the assertion fails while `errors.As` still finds it, because `errors.As` walks the whole unwrap chain. The other common mistake is string matching. Code like `strings.Contains(err.Error(), "no such host")` is fragile: the message text is not part of the API contract, and the wording differs between the two resolver implementations Go can use and between platforms. The boolean fields exist precisely so you do not have to do this. ## Practical shape A lookup wrapper in a real service usually looks like: call the resolver method with a context; on error, `errors.As` to `*net.DNSError`; classify into permanent (`IsNotFound`) versus retryable (`IsTimeout` or `IsTemporary`); log `Name` and `Server` so the on-call engineer can see which nameserver gave the answer; and surface a permanent failure loudly rather than retrying it forever. One subtlety about cancellation: when the lookup is being served by the C library rather than Go's own resolver, cancelling the context makes the Go call return promptly, but the underlying C call cannot be interrupted and finishes in its own time. Your code is unblocked; the work underneath is not.
- Why is matching on the text `no such host` a bad way to detect a missing name?The message string is not part of the API contract. Its exact wording varies between Go's own resolver and the C library's, and across platforms, so a substring match silently stops working when the environment changes. `*net.DNSError` exposes `IsNotFound` for exactly this reason — a boolean that means the resolver answered authoritatively that the name has no records.
- What is the difference between net.LookupHost and net.LookupIP?They do the same resolution and differ only in the result type. `LookupHost` returns `[]string` — addresses already formatted as text, handy for logging or building an address with `net.JoinHostPort`. `LookupIP` returns `[]net.IP`, the parsed form, which you want if you are going to inspect the address, filter by family, or dial it. Both go through `net.DefaultResolver`.
- How do you make a hostname lookup cancellable?Use the method on a resolver rather than the package function: `net.DefaultResolver.LookupHost(ctx, host)` takes a `context.Context`, so a deadline or cancellation applies. `net.LookupHost` has no context parameter and cannot be cancelled. Note that when the C library is serving the lookup, cancelling returns control to your goroutine promptly but does not abort the underlying C call.
saying these in an interview costs you the question
- Says a nonexistent hostname returns an empty slice and a nil error
- Detects a missing name by substring-matching the error text
- Retries an IsNotFound failure as if it were transient
- Uses a plain type assertion on a wrapped lookup error instead of errors.As
- Thinks net.LookupHost takes a context and can be cancelled