skip to content

In Go, what is the scope of a variable declared in an if statement's init clause?

level: middleimportance: should knowfreq 54%

answer

  1. an invisible block wraps the statement
  2. the else branch is inside it too
  3. it ends with the closing brace
  4. same shape for for and switch
  5. each case clause is its own block

basics

~20 s

It covers the whole if statement: the condition, the if body, and every else-if and else branch. It does not exist before or after the statement, because the init clause declares into an implicit block wrapping the entire if.

solid answer

~50 s

Go treats `if v, err := parse(s); err == nil { … } else { … }` as living inside an implicit block that wraps the whole statement. The variables declared in the init clause are visible in the condition, in the if body, and in every chained else-if condition and else branch — and nowhere after the closing brace. The same shape applies to a `for` init statement, whose variables are scoped to the loop statement, and to a `switch` init statement, whose variables are visible in the switch expression and in every case clause; each case clause is itself an implicit block, so a declaration inside one case does not leak into the next. This is the idiomatic way to keep a value's lifetime as short as the check that uses it, and it is also the reason `if err := do(); err != nil` never touches an outer `err` of the same name.

code

go · 6 lines
go
if v, err := strconv.Atoi(s); err == nil {
	fmt.Println(v)
} else {
	fmt.Println("bad input:", err) // v and err are in scope here too
}
// neither v nor err exists past this point

go deeper

for a junior

Know the shape if err := do(); err != nil and be able to say that err disappears after the statement. Recognising why that line is preferred over declaring err above the if is enough at this level.

for a middle

Explain the implicit block that wraps the statement, and extend it to for and switch — including that each case clause is its own block. An interviewer expects you to state exactly which branches see the init variables.

for a senior

Show judgment about when the narrow scope helps and when it hurts: values needed after the decision must be declared outside, and an else-if chain that redeclares err deserves a comment or a rename before it reaches review.

for a principal

Be ready to argue the readability tradeoff at codebase scale: init clauses shrink lifetimes and reduce accidental reuse, but chained branches that each redeclare the same name are a maintenance cost your conventions should address.

## The implicit block Go's block structure is mostly visible in the braces you type, with a few blocks the language inserts for you. One of them wraps each `if`, `for` and `switch` statement. The init clause — the short statement before the semicolon — declares into that implicit block, not into the surrounding function body and not into the statement's own body. That single rule answers the whole question: ```go if v, err := strconv.Atoi(s); err == nil { fmt.Println(v) } else { fmt.Println("bad input:", err) // v and err are in scope here too } // neither v nor err exists past this point ``` `v` and `err` are visible in the condition, in the body, and in the else branch, because all three sit inside the implicit block. They are not visible after the statement, because the implicit block ends there. ## Chained branches Else-if chains are, syntactically, an `if` statement whose else branch is another `if` statement. Each link may bring its own init clause, and each declaration is visible from its own point onward through the rest of that chain: ```go if a, err := first(); err != nil { // a and err } else if b, err := second(); err != nil { // b, and the second err, which shadows the first } else { // a is still visible; err here is the second one } ``` This is a legitimate place where shadowing appears without anyone typing a nested `:=` on purpose: the second init clause declares a second `err` in a deeper implicit block, and from that point on the name refers to it. It is fine when each branch only cares about its own error, and a trap when someone expects the outer one to have been updated. ## `for` and `switch` - **`for`**: `for i := 0; i < n; i++ { … }` declares `i` in the loop's implicit block. It is visible in the condition, the post statement and the body; it is gone after the loop. `for k, v := range m { … }` behaves the same way for `k` and `v`. - **`switch`**: `switch x := classify(v); x { … }` puts `x` in scope for the switch expression and for every case clause. Each case clause is *itself* an implicit block, so `y := …` inside one case is invisible to the next case — which is why Go needs no `break` to stop declarations leaking, and why two cases may declare the same name independently. - **Type switches** add a twist worth knowing: `switch v := x.(type)` declares a distinct `v` in each case clause, with the type of that clause. ## Why the idiom exists The init clause is Go's way of saying "this value exists only for the duration of this decision". Compared with declaring above the statement, it has two practical benefits: 1. **Narrow lifetime.** The name cannot be misused fifty lines later, and a later reader does not have to scan for other writes to it. 2. **No accidental interference with an outer variable.** `if err := f(); err != nil` deliberately does not touch an outer `err`. That is a feature when the outer value is not supposed to change, and it is exactly the trap when it is — the same mechanism read two ways. ## The consequence to keep in mind Because the scope ends with the statement, anything the branch computes that you need afterwards must be declared outside: ```go // wrong: v is gone after the statement if v, err := parse(s); err == nil { _ = v } // right: declare where it is needed v, err := parse(s) if err != nil { return err } use(v) ``` So the init clause is the right tool when the value is only interesting to the test, and the wrong one when the value is the point of the call. Choosing between the two forms on that basis is most of what good scoping looks like in day-to-day Go.

  • In a Go switch with an init statement, can two different case clauses declare the same name?
    Yes. Each case clause is its own implicit block, so `n := …` in one case is completely independent of `n := …` in another; neither is visible outside its clause. The init statement's own variables sit one level further out, so they are visible in the switch expression and in every clause.
  • When should you not use an if init clause?
    When the value the call produces is needed after the decision. Declaring it in the init clause scopes it to the statement, so you would have to recompute or restructure. Use the init clause for values that only inform the test — a lookup's ok flag, an error you either return or ignore — and a plain declaration above the statement for values the rest of the function uses.
  • Does an else-if chain's second init clause shadow the first one's variables?
    Yes, if it reuses a name. The else branch holds another if statement in a deeper implicit block, so `else if b, err := second(); …` declares a second `err` that hides the first from that point on. Each branch then sees its own error, which is usually what you want but is worth naming explicitly in review.

saying these in an interview costs you the question

  • Says the init variable is visible after the if statement
  • Claims it is confined to the if body and not the else
  • Thinks the init clause assigns to an outer variable of the same name
  • Believes a declaration in one switch case leaks into the next
  • Confuses the init clause with a package-level declaration