Conditional expression: q.Tern¶
q.Tern(cond, ifTrue, ifFalse) is the conditional-expression sugar Go's syntax doesn't have. Returns ifTrue when cond is true; otherwise ifFalse. Only the matching branch is evaluated.
display := q.Tern(user != nil, user.Name, "anonymous")
// → "anonymous" when user is nil; user.Name when not
Signature¶
Three args, strict types — the kind of signature gopls likes.
Why a preprocessor pass for what looks like a runtime helper¶
A naïve runtime implementation would always evaluate BOTH branches (Go's standard arg-evaluation semantics). That breaks the whole point: q.Tern(user != nil, user.Name, "anonymous") would panic-deref on a nil user before Tern ever got to choose a branch.
The preprocessor rewrites every call site to an IIFE that splices each branch's source text into its own arm — so a branch is only evaluated when its arm is taken. The rewrite for the example above:
user.Name lives only inside the if body. When user is nil the if-branch never runs and "anonymous" is returned. No nil-deref.
What you get¶
- Lazy evaluation of both branches. Side-effects, expensive calls, nil-deref-prone field access — only the taken branch runs.
- Single-eval of
cond.condevaluates exactly once, at the IIFE'sif. Same evaluation point you'd get with a hand-writtenif. - Chains naturally. Nested
q.Terncalls rewrite cleanly because the inner call's IIFE becomes the outer's branch text. Use this for multi-way picks where aswitchwould be heavier than the call site warrants. - No runtime overhead. The IIFE is one closure call. Go's escape analysis usually inlines it.
When to reach for q.Tern vs plain if¶
Use q.Tern when:
- You need a value-returning conditional in an expression position (struct literal field, function arg, return statement, etc.) where Go's statement-shaped
ifdoesn't fit. - The expression is short enough to fit on one line — or chains of
q.Terngive a cleaner multi-way pick than aswitch.
Reach for plain if/else when:
- The branches are large enough that the IIFE form hurts readability.
- You need any control-flow shape beyond pick-one (break/continue/return mid-branch, etc.).
For a multi-arm value-returning conditional with predicates and exhaustive checks, q.Match is purpose-built.
Examples¶
// Field defaults via lazy nil-deref:
displayName := q.Tern(user != nil, user.Name, "anonymous")
maxConn := q.Tern(cfg != nil, cfg.MaxConn, defaultMaxConn)
// Lazy expensive computation — slowLookup() only when cache misses:
v := q.Tern(cached, fast(), slowLookup(key))
// In a struct literal (Go's plain `if` doesn't fit here):
req := Request{
Timeout: q.Tern(opts.Timeout > 0, opts.Timeout, defaultTimeout),
Endpoint: q.Tern(opts.Endpoint != "", opts.Endpoint, defaultEndpoint),
}
// In a return statement:
func sign(n int) int {
return q.Tern(n > 0, 1, q.Tern(n < 0, -1, 0))
}
// Chained for multi-way pick — nested terns nest cleanly:
tier := q.Tern(score >= 90, "A",
q.Tern(score >= 80, "B",
q.Tern(score >= 70, "C", "F")))
// Explicit T when you want to widen the result type:
var iface fmt.Stringer = q.Tern[fmt.Stringer](ok, concreteImpl, fallbackImpl)
Caveats¶
condis always evaluated. It has to be — that's how the if knows which branch to take. Lazy semantics apply only to the branch values.- Both branches must agree on type. Go's type inference resolves
Tfrom the branches; mismatched types fail at compile time. - Untyped constants follow Go's default-type rule.
q.Tern(cond, 1, 2)infersTasint. If you need a different type, widen at the call site (q.Tern(cond, int64(1), int64(2))) or use the explicit form (q.Tern[int64](cond, 1, 2)).