Go Specification: Short Statement in If¶
Source: https://go.dev/ref/spec#If_statements Sections: If statements, Switch statements, For statements, Blocks, SimpleStmt
1. Spec Reference¶
| Field | Value |
|---|---|
| Official Spec | https://go.dev/ref/spec#If_statements |
| Switch with init | https://go.dev/ref/spec#Switch_statements |
| Type switch with init | https://go.dev/ref/spec#Type_switches |
| For with init | https://go.dev/ref/spec#For_statements |
| SimpleStmt definition | https://go.dev/ref/spec#Statements |
| Block / Implicit block | https://go.dev/ref/spec#Blocks |
| Go Version | Go 1.0 (the form has existed since the language's first release) |
| Effective Go reference | https://go.dev/doc/effective_go#if |
Official text from the spec:
"If statements specify the conditional execution of two branches according to the value of a boolean expression. If the expression evaluates to true, the 'if' branch is executed, otherwise, if present, the 'else' branch is executed.
The expression may be preceded by a simple statement, which executes before the expression is evaluated."
And from the Blocks section:
"Each 'if', 'for', and 'switch' statement is considered to be in its own implicit block."
"Each clause in a 'switch' or 'select' statement acts as an implicit block."
2. Definition¶
The short statement in if (also called the if-init form) is the optional simple statement that may appear immediately after the if keyword and before the boolean expression, separated by a semicolon. The form is:
The init runs before the condition is evaluated. Variables introduced by the init via short variable declaration are scoped to the implicit block formed by the if/else if/else chain — visible inside the condition, body, every else if, and the final else, but not after the chain's closing brace.
3. Grammar¶
From the Go spec:
Where SimpleStmt is:
Excluded from SimpleStmt: - Declarations (var, const, type) - Labeled statements - go, defer - return, break, continue, goto, fallthrough - Block, IfStmt, SwitchStmt, SelectStmt, ForStmt
Switch statements parallel:
ExprSwitchStmt = "switch" [ SimpleStmt ";" ] [ Expression ] "{" { ExprCaseClause } "}" .
TypeSwitchStmt = "switch" [ SimpleStmt ";" ] TypeSwitchGuard "{" { TypeCaseClause } "}" .
For:
ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] .
InitStmt = SimpleStmt .
PostStmt = SimpleStmt .
For-clause requires both semicolons even if init/condition/post are empty.
4. Core Rules & Constraints¶
4.1 Init Is Optional¶
When omitted, the leading ; is omitted as well. The grammar requires ; only when init is present.
4.2 Init Runs Before the Condition¶
The init x := 7 executes; then x > 5 is evaluated; then the body runs.
4.3 Init Is a SimpleStmt — Not a Declaration¶
A var x = 1 declaration is a Declaration, not a SimpleStmt, and is not allowed:
Use := (a ShortVarDecl, which is a SimpleStmt):
4.4 Implicit Block Scope¶
The if statement defines an implicit block that contains the init's declarations. The body and every else branch are nested inside this implicit block. Names declared in init are visible inside the chain and only inside the chain.
if v, ok := m[k]; ok {
use(v) // v, ok visible
} else {
fallback() // v, ok still visible
}
// v, ok out of scope
4.5 := in Init Always Shadows¶
The implicit block is empty when the init runs. A short variable declaration in init introduces all names on the LHS into this fresh scope, regardless of any same-named variables in the surrounding scope.
x := 10
if x := x + 1; x > 5 { // inner x shadows outer
fmt.Println(x) // 11
}
fmt.Println(x) // 10 (outer untouched)
4.6 = in Init Reuses Existing Names¶
When the init uses = (an Assignment), no new variable is introduced. All names on the LHS must already exist in scope.
This is the way to update an outer variable (e.g., a named return) from inside the init.
4.7 Multiple Names in Init¶
Multi-variable short declarations are allowed:
All declared names share the implicit block's scope.
4.8 Switch and Type Switch Init¶
Switch statements may have an init using the same syntax:
switch x := f(); x {
case 1: ...
case 2: ...
}
switch x := f(); { // tagless switch
case x > 0: ...
case x < 0: ...
}
switch x := f(); v := x.(type) { // type switch
case int: ...
case string: ...
}
The init's variables are scoped to the switch's implicit block.
4.9 For Init¶
The for statement's init slot is a SimpleStmt:
The init's variables are scoped to the for's implicit block (covering condition, body, post). For-clause requires both semicolons even if any of init/cond/post are empty.
4.10 Compiler Behavior¶
After parsing and type-checking, the compiler treats the init form as if the init were a separate statement preceding the if. SSA, register allocation, and machine code are identical to the hoisted form. There is no runtime cost or benefit; the form is purely syntactic.
5. Edge Cases¶
5.1 Init With No Useful Side Effect¶
A function call whose results are ignored is a legal init:
But the spec restricts ExpressionStmt to function calls, method calls, and receive operations. A bare identifier or arithmetic expression is not a valid ExpressionStmt:
x := 5
if x; x > 0 { ... } // compile error: x evaluated but not used
if x + 1; x > 0 { ... } // compile error: same reason
Avoid init forms whose only purpose is to call a function whose result is discarded — they hide whether the call mattered.
5.2 Empty Init¶
The grammar permits an empty SimpleStmt, but gofmt removes the leading ; if empty. In practice, you never see if ;cond { ... }.
5.3 Increment/Decrement in Init¶
i must already be in scope. The i++ runs as init, then condition is evaluated.
5.4 Send in Init¶
Sends to ch, then evaluates cond. Rare in practice.
5.5 Init in else if¶
The else if opens its own nested implicit block. b is in scope for the else if's condition, body, and any subsequent else/else if. a from the outer init is still in scope throughout.
5.6 Init With Function Call That Has No Result¶
Calls f for side effects, then evaluates cond. Rarely useful since you cannot capture the result without :=.
5.7 Init Variable Shadowed in Body¶
Inside the body, you can shadow the init variable with a deeper block:
Standard nested-scope behavior.
5.8 Init Cannot Reference Names From the Body¶
The init runs before the body, so it cannot reference names declared inside the body:
This is by definition — the body has not run yet.
5.9 Init Variables and Closures¶
A function literal inside the body that captures the init's variable does so by reference:
If f escapes, v escapes too. The init form does not affect escape analysis differently from a hoisted declaration.
5.10 Init In a select Case Body¶
select cases do not have an init slot themselves, but you can nest an if with init inside:
msg is bound by the case; v, ok are bound by the if-init. Two nested implicit blocks.
6. Type Rules¶
6.1 The Condition Must Be Boolean¶
The condition expression must be of type bool. Init does not change this:
6.2 Init's Variables Have Their Declared Types¶
Short declarations follow the standard type-inference rules:
Multi-return calls assign types per position:
6.3 Init's Names Cannot Conflict With Body Declarations¶
Inside the body, a re-declaration with := opens a new inner scope:
Or with = to mutate the init's x:
7. Related Specs¶
7.1 If statements¶
The primary reference. Defines the grammar, evaluation order, and scope of init.
7.2 Switch statements¶
Defines the parallel init form for switch. Same SimpleStmt ; shape.
7.3 Type switches¶
Defines switch x := y.(type) { ... } and the optional init: switch t := f(); v := t.(type) { ... }.
7.4 For statements¶
Defines the for-clause InitStmt ; Condition ; PostStmt, which uses SimpleStmt for both init and post.
7.5 Blocks¶
Defines explicit and implicit blocks. The implicit block of an if, switch, or for is what scopes the init's variables.
7.6 SimpleStmt¶
Defines SimpleStmt = EmptyStmt | ExpressionStmt | SendStmt | IncDecStmt | Assignment | ShortVarDecl.
7.7 Short variable declarations¶
Defines :=. The init form's most common shape uses this.
7.8 Assignments¶
Defines =. Init form may use this to mutate existing variables.
7.9 Effective Go: If¶
Style guide endorsement of the init form.
8. Version History¶
| Version | Change |
|---|---|
| Go 1.0 | If-init form present from initial release. Same for switch and for. |
| Go 1.0+ | No semantic changes to if-init. |
| Go 1.18 | Generics arrive but do not affect if-init syntax or scope. |
| Go 1.22 | Loop-variable per-iteration semantics — affects for only, not if-init. |
The if-init form has been stable since Go 1.0. Future Go versions are unlikely to change it; it is part of the language's core syntax.
9. Summary of Spec Rules¶
- Init is optional. Grammar:
if SimpleStmt ; Expression Block [ else ... ]. - Init must be a
SimpleStmt— not a declaration, return, break, etc. - Init runs before the condition.
- Init's
:=creates names in the implicit block (always shadows outer). - Init's
=mutates existing names (no new declaration). - Variables declared in init are visible across the entire if/else if/else chain and not after.
- The same form exists in
switch,type switch, andfor. - Each clause of a
switchorselectis a further implicit block. - The condition must be of type
bool. - Init form has no runtime cost difference vs a hoisted declaration.
These rules have been stable since Go 1.0 and are unlikely to change.
10. Spec Quotations Reference¶
A consolidated list of the exact spec text relevant to init form. All quotes are from https://go.dev/ref/spec.
From "If statements"¶
"If statements specify the conditional execution of two branches according to the value of a boolean expression. If the expression evaluates to true, the 'if' branch is executed, otherwise, if present, the 'else' branch is executed."
"The expression may be preceded by a simple statement, which executes before the expression is evaluated."
From "Switch statements"¶
"Switch statements provide multi-way execution. An expression or type is compared to the 'cases' inside the 'switch' to determine which branch to execute."
"Both a 'switch' statement and a 'type switch' statement may be preceded by a simple statement that executes before the expression is evaluated."
From "For statements"¶
"For statements with for clause have an init statement, a condition, and a post statement, all of which are optional."
From "Blocks"¶
"A block is a possibly empty sequence of declarations and statements within matching brace brackets."
"In addition to explicit blocks in the source code, there are implicit blocks: 1. The universe block encompasses all Go source text. 2. Each package has a package block containing all Go source text for that package. 3. Each file has a file block containing all Go source text in that file. 4. Each 'if', 'for', and 'switch' statement is considered to be in its own implicit block. 5. Each clause in a 'switch' or 'select' statement acts as an implicit block."
The spec rule (4) is the foundation of init's scope; (5) is why each switch/select case has its own further nested scope.
From "Statements" (SimpleStmt)¶
"SimpleStmt = EmptyStmt | ExpressionStmt | SendStmt | IncDecStmt | Assignment | ShortVarDecl ."
This is the exhaustive list of what may appear in init position.
From "Declarations and scope"¶
"The scope of a constant or variable identifier declared inside a function begins at the end of the ConstSpec or VarSpec (ShortVarDecl for short variable declarations) and ends at the end of the innermost containing block."
For init form, the "innermost containing block" is the implicit block of the if/switch/for. This is what makes the init's variables die at the closing }.
11. Worked Spec Compliance Examples¶
11.1 Init Variable Cannot Escape Block¶
package main
import "fmt"
func main() {
if x := 1; x > 0 {
fmt.Println(x)
}
// fmt.Println(x) -- compile error: undefined: x
}
Spec compliance: the implicit block ends at }; x was scoped to that block; references outside are unresolved.
11.2 Init Variable Visible In Else¶
Spec compliance: the else clause is part of the if's implicit block; the init's declarations cover both branches.
11.3 Init Variable Visible In Else-If Chain¶
if x := compute(); x > 5 {
// x = result
} else if x > 0 {
// x still visible
} else {
// x still visible
}
Spec compliance: an else-if is parsed as another IfStmt nested inside the outer if's else clause. The outer if's implicit block contains everything; x is in scope throughout.
11.4 Multiple Levels of Init¶
if a := 1; a > 0 {
if b := 2; b > a {
// a and b both visible
fmt.Println(a, b)
}
// only a visible here
}
// neither visible
Spec compliance: each if has its own implicit block. The inner if's block is nested inside the outer's. Names are visible in their declaring block and any nested blocks.
11.5 Init Variable Shadowed Inside Body¶
Spec compliance: the body is a nested block. The body's := introduces a new x in the body scope, shadowing the implicit block's x. After the body's closing }, the body scope ends; the init's x is reachable again (but the if has already finished, so this is moot).
12. Summary of Spec Rules¶
- Init is optional. Grammar:
if SimpleStmt ; Expression Block [ else ... ]. - Init must be a
SimpleStmt— not a declaration, return, break, etc. - Init runs before the condition.
- Init's
:=creates names in the implicit block (always shadows outer). - Init's
=mutates existing names (no new declaration). - Variables declared in init are visible across the entire if/else if/else chain and not after.
- The same form exists in
switch,type switch, andfor. - Each clause of a
switchorselectis a further implicit block. - The condition must be of type
bool. - Init form has no runtime cost difference vs a hoisted declaration.
These rules have remained stable since Go 1.0 and are unlikely to change in any future revision.