When should a Go function return `(T, bool)` in comma-ok style instead of `(T, error)`?
answer
- one bit versus a reason
- unset is not the same as empty
- can the caller act on why
- os.LookupEnv beside os.Getenv
- never squeeze a diagnosis into false
basics
~10 sReturn a bool when absence is ordinary and has one uninteresting cause, as with os.LookupEnv. Return an error when there are several distinguishable causes, or the caller must report why it failed.
solid answer
~50 sThe comma-ok shape `(T, bool)` says "here is the value, and whether there was one". It fits when absence is a normal outcome with a single, self-evident cause — `os.LookupEnv("PORT")` returns `("", false)` for an unset variable, which is why it exists alongside `os.Getenv`, whose lone string cannot tell an unset variable from one set to the empty string. `(T, error)` fits when the failure has *causes*: several ways to go wrong, or a message worth surfacing. The test is whether the caller could do anything useful with a reason. If the honest answer is "it just is not there", a bool is clearer; if it is "the file was unreadable, or line 12 had a bad duration", a bool throws away what the caller needs. The failure to avoid is squeezing a diagnosis into a bool — a validator returning `false` tells CI something is wrong and nothing about what.
code
go · 11 linesfunc port() (int, error) {
raw, ok := os.LookupEnv("PORT")
if !ok {
return 8080, nil // absent is ordinary: the bit was enough
}
n, err := strconv.Atoi(raw)
if err != nil {
return 0, err // a bad value has a cause the caller must see
}
return n, nil
}go deeper
Recognise both shapes when you read them: a second result of bool means the value may not exist, and a second result of error means it may have failed. Check it before using the first result either way.
Explain the criterion — one uninteresting cause versus several distinguishable ones — and cite os.LookupEnv against strconv.Atoi. Know that the flag goes last, means presence, and pairs with a zero value when false.
Show the review instinct: spot a function that already holds an error and returns a bool, and say what that costs the person reading the failure output. Be ready to argue a signature change rather than patching the branch.
Treat it as a package-surface policy. Once callers outside your team depend on (T, bool), widening it to (T, error) is a breaking change, so decide early which lookups can never grow a second failure mode.
## Two conventions, one question Go has no exceptions and no optional type, so a function that might not produce a value says so in its result list. Two shapes dominate: ```go func lookup(key string) (Value, bool) // comma-ok: is there one? func parse(s string) (Value, error) // error: why did it fail? ``` Both are idiomatic. Choosing between them is one of the small API decisions a reviewer will push back on, and the criterion is not difficulty or importance — it is **whether the reason is information**. ## The comma-ok shape: absence is ordinary A `bool` second result carries exactly one bit: the value is real, or it is not. That is the right amount of information when there is only one way to come up empty and the caller already knows what it is. The canonical example is the environment: ```go raw, ok := os.LookupEnv("PORT") ``` `os.Getenv` returns only a string, so an unset variable and one set to `""` are indistinguishable; `os.LookupEnv` adds the bit that separates them. There is nothing to explain — the variable is either in the environment or it is not — so an `error` would be ceremony wrapped around a fact the caller can already state. The shape also reads well at the call site, because Go lets the two-value form drive an `if` with an initialiser: ```go if raw, ok := os.LookupEnv("PORT"); ok { // raw is meaningful only inside this block } ``` The scoping is part of the appeal: the value and the flag that validates it live and die together, so it is hard to use one without having checked the other. When you write your own function in this style, follow the convention exactly: the flag goes **last**, it is named for truth (`ok`, `found`, `present`) rather than for failure, and `true` means the value is usable. A second result that means "something went wrong" inverted from the usual sense will be misread by everyone who has ever written `v, ok :=`. One more rule that is easy to get wrong: when the flag is `false`, return the **zero value** for `T`, not a partially filled one. Callers are entitled to ignore the first result entirely when the second is false, and half-built values leak into code that skipped the check. ## The error shape: the reason is the payload As soon as there is more than one way to fail, or the failure has detail a human or a caller needs, a bool destroys information that cannot be recovered. Consider a config validator that runs as a CI step: ```go func loadConfig(path string) (Config, bool) // "invalid" — but why? func loadConfig(path string) (Config, error) // "open /etc/app.yaml: permission denied" ``` The first version compiles, passes tests, and produces a CI step whose entire output is a red X. The file might be missing, unreadable, malformed, or valid but with an out-of-range timeout, and the engineer on the pull request has to reproduce it locally to find out which. The second version hands the reason to the log line for free. The same reasoning applies inside a function. Reducing an `error` you already hold down to a `false` is the most common way this convention is misused — you had the diagnosis and threw it away. If you catch yourself writing `if err != nil { return zero, false }`, that is the signal to change the signature rather than the branch. ## Cases at the boundary - **Pure, total lookups over data you already have** — an index, a registry, a parsed set — are comma-ok. Failure is "not present", which is not a failure at all. - **Anything touching the filesystem, network, clock, or a parser** is almost always `error`, because the world has many ways to say no. - **Do not return `(T, bool, error)`.** Three results asking the caller to check two conditions is a signature that has not decided what it means. Pick one; if you truly need "absent" and "broken" as different outcomes, an `error` value the caller can test for a specific sentinel case expresses it in two results. - **Do not make the bool mean "error"**, and do not order it first. The convention is load-bearing: readers pattern-match on `v, ok :=` and stop reading. ## What interviewers listen for A strong answer names a real example on each side (`os.LookupEnv` versus `strconv.Atoi`), states the criterion as "can the caller do anything with the reason", and volunteers the anti-pattern of collapsing a known error into a bool. A weak answer treats the choice as taste, or claims `error` is always the safer default — which produces functions whose "error" for an absent optional key forces every caller to compare against a sentinel to find out that nothing is wrong.
- Why does the standard library provide both os.Getenv and os.LookupEnv?`os.Getenv` returns a single string, so an unset variable and one set to the empty string both come back as `""`. `os.LookupEnv` adds a second result that distinguishes them. It is the textbook case for comma-ok: the extra bit carries the only information a caller could want, and there is no reason worth phrasing as an error.
- What should the first result be when a comma-ok function returns false?The zero value of its type. Callers are entitled to ignore the value entirely once the flag is false, so anything partially built will eventually be used by code that skipped the check. Returning a half-populated struct with `false` is a bug waiting for the first caller who trusts the value.
- Is `(T, bool, error)` ever a reasonable Go signature?Almost never. It asks every caller to check two conditions and leaves the relationship between them undefined — what does `false` with a nil error mean? Decide which distinction matters. If "absent" and "broken" are genuinely different outcomes, express both through the error result so there is one thing to check.
saying these in an interview costs you the question
- Says error is always the safer default result
- Collapses an error it already holds into false
- Puts the ok flag first instead of last
- Makes the bool mean failure rather than presence
- Returns a half-filled value alongside false
- Designs a (T, bool, error) signature