skip to content

Inside {{range .Items}} in a Go template, what happens to dot, and how do you reach the top-level value?

level: middleimportance: must knowfreq 60%

answer

  1. the cursor moves, the root does not
  2. one dollar sign, set once per execution
  3. name it before you descend
  4. with is a nil guard that also rebinds
  5. range else fires on zero iterations

basics

~20 s

Inside a range body dot is rebound to the current element, so outer fields are unreachable through it. Use $, which stays bound to the value passed to Execute, or capture what you need in a variable before the loop.

solid answer

~40 s

`range` and `with` both rebind dot for the length of their body. Inside `{{range .Items}}` dot is the current element, so `{{.SKU}}` reads the element's field and `{{.Currency}}` — a field of the outer struct — fails with a "can't evaluate field" error. Two escapes exist: `$` is set to the argument given to `Execute` and stays the root for the whole execution, so `{{$.Currency}}` works anywhere; or you bind your own variable before entering the loop, `{{$cur := .Currency}}`, and use `{{$cur}}` inside. You can also name the loop variables: `{{range $i, $item := .Items}}` keeps dot rebound but gives you the index and element by name. `with` is the same story for a single value: `{{with .Customer}}{{.Email}}{{end}}` runs the body only when the value is non-empty, with dot set to it.

code

go · 14 lines
go
type Order struct {
	Currency string
	Items    []struct{ SKU string }
}

const src = `{{range .Items}}{{.SKU}} in {{$.Currency}}
{{else}}no items
{{end}}`

t, err := template.New("order").Parse(src)
if err != nil {
	return err
}
return t.Execute(os.Stdout, o)

go deeper

for a junior

Remember the rule rather than the theory: inside range, dot is the element. If you need something from the outer payload, write $.Field or copy it into a variable before the loop.

for a middle

Explain rebinding precisely for both range and with, what $ is bound to and when it is set, and how {{range $i, $v := ...}} differs from bare range. Know that range's else covers zero iterations.

for a senior

Demonstrate the failure mode difference: a missing struct field stops Execute, a missing map key renders <no value> and ships. Show the habit of naming each level you descend through so deep templates stay reviewable.

for a principal

Argue for a house style — named variables over bare dot in anything nested, and payload types that are structs rather than untyped maps — because that choice decides whether a template bug is an error or a silently wrong document.

## Dot is a cursor, and some actions move it At the start of execution dot is the value handed to `Execute`. Two actions move it for the duration of their body, and this is the single most common source of confusion in Go templates. ### range ``` {{range .Items}}{{.SKU}} x{{.Qty}} {{end}} ``` `range` accepts an array, slice, map, channel (and, in recent Go, an integer or an iterator function). For each element it executes the body once with **dot set to that element**. Inside the body, `.Items` no longer exists — dot is an element, not the outer struct. Referring to an outer field there produces an execution error such as `can't evaluate field Currency in type main.Item`. With a map, the body runs once per key in sorted key order when the keys are of a basic ordered type — a deliberate difference from Go's randomised `for range` over a map, and useful when you are rendering something that must be reproducible. `range` also takes an `{{else}}` branch, which runs exactly once when the ranged value has **zero** iterations — an empty or nil slice, an empty map: ``` {{range .Items}}...{{else}}No items.{{end}} ``` ### with ``` {{with .Customer}}Hi {{.Name}}{{end}} ``` `with` evaluates its pipeline once. If the value is *empty* (the same emptiness rule `if` uses: false, 0, a nil pointer or interface, a zero-length array, slice, map or string) the body is skipped entirely and the optional `{{else}}` branch runs. Otherwise dot is set to that value for the body. So `with` is simultaneously a nil guard and a scope shortener, which is why it reads so well for optional nested structs. ## Getting back out: $ `$` is a variable the template engine pre-declares at the start of each execution and sets to the data argument. It is never rebound by `range` or `with`, so it is the reliable root: ``` {{range .Items}}{{.SKU}} priced in {{$.Currency}} {{end}} ``` One caveat worth knowing: `$` is the root *of the current execution*. When a nested template is invoked with its own argument, `$` inside it is that argument, not the outermost payload. If you need the outer root deeper down, pass it explicitly. ## Getting back out: your own variables A template variable is declared with `:=` and lives to the end of the enclosing block: ``` {{$cur := .Currency}} {{range .Items}}{{.SKU}} {{$cur}} {{end}} ``` This is often clearer than `$.Currency` because it names the thing at the point where the reader can see what it refers to, and it survives being moved into a nested template later. Range can declare variables of its own: - `{{range $item := .Items}}` — one variable, the element. - `{{range $i, $item := .Items}}` — index (or map key) and element. Dot is still rebound in both forms; the variables are an addition, not a replacement. Many teams write `{{range $i, $item := .Items}}` and then use `$item.SKU` throughout, precisely so that nothing in the body depends on what dot happens to be. ## Why this bites The failure is asymmetric and that is what makes it interview-worthy. If the outer field is a struct field, the mistake is loud: `Execute` returns an error and the render stops. If dot inside the loop is a `map[string]any` — very common in payloads assembled by hand — the missing key is not an error by default; it renders as `<no value>` and the wrong output ships. Same typo, two completely different consequences. Nesting compounds it: inside `{{range .Orders}}{{range .Lines}}`, dot is a line, `$` is still the whole payload, and the order in between is reachable only if you named it — `{{range $order := .Orders}}` — before descending. That habit, naming the level you will need later, is the practical takeaway. ## Quick summary - `range` sets dot per element; `{{else}}` covers zero iterations. - `with` sets dot to a non-empty value and skips the body otherwise. - `$` is the execution's root argument and is never rebound by those actions. - Named variables (`{{$x := ...}}`, `{{range $i, $v := ...}}`) are the readable escape from dot entirely.

  • What does the {{else}} branch of a range action run on, and how is that different from an if?
    `{{range .Items}}A{{else}}B{{end}}` runs B exactly once when the ranged value produces **zero** iterations — a nil or empty slice, an empty map. It is not a per-iteration else. An `if` else, by contrast, tests one pipeline for emptiness and picks a branch. The range form exists so the common "list, or an empty-state message" shape needs no separate emptiness test.
  • Inside a nested {{range .Orders}}{{range .Lines}}, how do you reach the enclosing order?
    Not through `$`, which is the whole payload, and not through dot, which is a line. Name the level on the way down: `{{range $order := .Orders}}{{range .Lines}} ... {{$order.ID}} ... {{end}}{{end}}`. Template variables are lexically scoped to the block that declares them, so `$order` remains visible in the inner loop. Naming levels as you descend is the habit that keeps deep templates readable.
  • Why is with both a scoping tool and a guard?
    `{{with .Customer}}` evaluates the pipeline once, skips the whole body when the value is empty by the template emptiness rule — including a nil pointer — and otherwise rebinds dot to it. So it removes a repeated prefix (`.Customer.Name`, `.Customer.Email` become `.Name`, `.Email`) and simultaneously prevents rendering a block for an absent nested value. `{{else}}` supplies the absent case.

saying these in an interview costs you the question

  • Thinks outer fields stay reachable through dot inside range
  • Believes $ is rebound by range or with
  • Says range else runs once per skipped element
  • Assumes with runs its body even for an empty value
  • Uses $.Field deep in nested loops expecting the enclosing element