skip to content

What counts as false for {{if}} in a Go template, and how do you compare two values there?

level: middleimportance: should knowfreq 45%

answer

  1. emptiness, not a boolean
  2. length zero counts, nilness does not
  3. no operators anywhere in the language
  4. the word false is a non-empty string
  5. zero and absent look identical

basics

~20 s

Go templates test emptiness, not a boolean: false, 0, a nil pointer or interface, and any zero-length array, slice, map or string are empty. Comparison uses functions such as eq, ne, lt and gt, never operators.

solid answer

~40 s

`{{if pipeline}}` does not require a `bool`. It evaluates the pipeline and asks whether the value is **empty**: false, 0 of any numeric kind, a nil pointer or nil interface, or an array, slice, map or string of length zero. Anything else — including the string `"false"` and a struct value — is true. That makes `{{if .Items}}` a natural "do I have any" test, and it also means `{{if .Count}}` quietly hides a legitimate zero. There are no comparison operators in the template language; comparisons are builtin functions: `eq`, `ne`, `lt`, `le`, `gt`, `ge`, plus `and`, `or` and `not`, all in prefix form: `{{if gt .Retries 3}}`, `{{if and .User .User.Active}}`. Note that `and` and `or` evaluate their arguments in order and return the first decisive one. `{{else if}}` chains, and `{{else}}` closes.

code

go · 4 lines
go
const src = `{{if .Items}}{{len .Items}} item(s){{else}}empty{{end}}
{{if gt .Retries 0}}retried {{.Retries}}x{{end}}
{{if and (ne .Status "") (eq .Status "active")}}live{{end}}
`

go deeper

for a junior

Learn the list of empty values by heart: false, 0, nil pointer or interface, and zero-length array, slice, map or string. Remember comparisons are words — eq, lt, gt — not symbols.

for a middle

Explain that if and with share one emptiness rule, why a non-empty string like "false" is true, and how pipelines feed a value in as the last argument with parentheses for nesting.

for a senior

Show judgment about where the decision lives: collections tested with bare if, scalars tested explicitly, and anything with nested and/or computed in Go and passed in as a boolean so it can be unit-tested.

for a principal

Own the boundary between template and code. Argue for view models that carry pre-computed display flags, so templates stay declarative and business rules stay in testable Go rather than accreting in untestable markup.

## The emptiness rule `{{if pipeline}}...{{else}}...{{end}}` in Go's template language does not need a boolean. It evaluates the pipeline once and applies a single documented rule: the value is treated as false when it is **empty**, meaning - the boolean `false`, - any numeric zero (integer, float, complex), - a nil pointer or a nil interface value, - an array, slice, map or string of length zero. Everything else is true. `{{with}}` uses exactly the same rule to decide whether to run its body, which is why the two actions feel like relatives. Several consequences catch people out: - The **string `"false"` is true** — it is a non-empty string. Rendering a boolean into a string somewhere upstream and testing it in the template inverts your logic. - A **struct value is always true**, even a zero struct, because structs are not in the empty list. `{{if .Address}}` on a `struct` field never guards anything; on a `*Address` it guards correctly. - `{{if .Count}}` conflates "absent" with "zero". If zero is a meaningful value — a count, a price, a temperature — test what you actually mean: `{{if gt .Count 0}}`, or pass a pointer, or pass a separate `HasCount` field. - An empty non-nil slice and a nil slice both test false, because the rule is about length, not nilness. ## Comparison is a function call The template language has no operators at all — no `==`, no `<`, no `&&`. Comparisons are builtin functions in prefix position: - `eq arg1 arg2` (and `eq a b c` is true when `a` equals any of `b`, `c`), - `ne`, `lt`, `le`, `gt`, `ge`, - `not x`, and the short-circuiting `and`/`or`. So `{{if eq .Status "active"}}`, `{{if ge .Score 90}}`, `{{if and .User .User.Active}}`. The comparison builtins require *comparable basic types* of matching kinds: comparing a signed integer with an unsigned one, or a string with a number, is an execution error rather than a silent false. That strictness is a feature — it catches payloads whose shape drifted. `and` and `or` are not plain functions in one respect: they evaluate arguments left to right and stop at the first decisive one, returning that argument (not a bare boolean). `{{if and .User .User.Active}}` therefore does not dereference `.User.Active` when `.User` is nil — which is the idiomatic nil-safe conjunction. `{{or .Nickname .Name}}` is the common "first non-empty wins" default. ## Pipelines A pipeline is a sequence of commands separated by `|`. The result of each command becomes the **final argument** of the next: ``` {{.Body | myFilter}} is myFilter .Body {{len .Items}} plain call, no pipe needed {{if gt (len .Items) 3}} parenthesised call as an argument ``` Two details matter. First, the piped value goes on the *end* of the argument list, so `{{.X | f "a"}}` calls `f "a" .X`. Second, parentheses group a call so its result can be used as an argument — that is how you nest, since there is no operator precedence to lean on. `{{if and (gt .Retries 0) (lt .Retries 5)}}` is the standard shape. A pipeline's value is also what an `if`, `with` or `range` tests, and what a variable assignment captures: `{{$n := len .Items}}`. ## Design guidance The emptiness rule is convenient for the 90% case — "render this block if there is something to render" — and treacherous for anything where zero, false or an empty string is a legitimate value you must display differently from absent. The rule of thumb is: use bare `{{if .Items}}` for collections, and use an explicit comparison or a dedicated boolean field for scalars. If a template needs more than one nested `and`/`or`, that is usually a signal to compute the decision in Go, where it can be unit-tested, and hand the template a plain boolean field.

  • Why is {{if .Count}} a poor test when Count is an integer?
    Because the emptiness rule makes 0 false, so "the field is genuinely zero" and "there is nothing here" render identically. If zero is meaningful, say what you mean: `{{if gt .Count 0}}`, or hand the template a `*int` so nil means absent, or add an explicit boolean field computed in Go. The same trap applies to prices, temperatures and any value whose zero is real.
  • What does the | operator do to the value on its left in a template pipeline?
    It passes it as the **final** argument of the command on its right, so `{{.X | f "a"}}` means `f "a" .X`. Commands chain left to right, and the last command's result is what gets written, tested by `if`, or assigned to a variable. When you need a call's result as an inner argument instead, parenthesise it: `{{if gt (len .Items) 3}}`.
  • How do and and or behave when an argument would panic?
    They evaluate arguments left to right and stop at the first decisive one, returning that argument rather than a plain boolean. So `{{if and .User .User.Active}}` never evaluates `.User.Active` when `.User` is empty, which makes it a safe nil guard, and `{{or .Nickname .Name}}` yields the first non-empty value as a default.

saying these in an interview costs you the question

  • Assumes if requires a bool like Go's own if
  • Thinks the string "false" tests false
  • Uses {{if .Count}} where zero is a real value
  • Writes == or && inside a template action
  • Expects a zero struct value to test false