Scope & Shadowing
Scope determines where a name is visible. Go's rules are intuitive, the innermost block that contains a declaration is its scope. The subtle gotcha is shadowing: declaring a new variable with the same name as an outer one, which is legal, common, and occasionally catastrophic.
Learning objectives
- Identify package-level, function-level, and block-level scopes.
- Explain how
{}creates a new scope. - Spot variable shadowing and know when it's a bug.
- Use
go vet -vettoolorshadowto catch shadowing.
Scope levels
Top to bottom, narrowest at the bottom:
- Universe: built-ins like
int,len,nil,true. - Package: any name declared at file level, in any file of the package.
- File: imported package names.
- Function: parameters and results.
- Block: any
{ ... }including the body ofif,for,switch.
Block scope
package main
import "fmt"
var package = "hi" // package scope
func main() {
x := 10 // function scope
if y := 20; y > x { // y: if-scope
z := 30 // z: only in this block
fmt.Println(x, y, z)
}
// y and z are not accessible here
}
The short-statement form of if/for/switch
introduces a variable scoped to that statement only, a pattern we've
used all over this course.
Shadowing
x := 1
{
x := 2 // a NEW variable named x, shadows the outer one
fmt.Println(x) // 2
}
fmt.Println(x) // 1: outer x unchanged
Perfectly legal. Sometimes useful. Sometimes a catastrophe.
The classic trap
This one bites everyone:
func readFile(path string) error {
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()
if info, err := f.Stat(); err == nil { // ⚠ new err!
fmt.Println(info.Size())
}
// If we reached here assuming err from Stat was handled... we didn't.
// The err we see now is still the OUTER one from os.Open.
return err
}
The := in the nested if declares a new
err inside the if-block because at least one of the names
on the left (info) is new. The outer err is
never updated.
Fix: pre-declare info or use =:
var info os.FileInfo
info, err = f.Stat()
if err != nil { ... }
Detecting shadowing
go vet doesn't check shadowing by default. Install the
shadow analyzer:
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
shadow ./...
It flags every shadowed variable. Running it in CI is an excellent habit on any non-trivial project.
Style tips
- Declare as late as possible. Put variables close to where they're used; the shorter the lifespan, the less trouble.
- Prefer short-statement scope for errors.
if err := f(); err != nil { ... }keepserrout of the surrounding function. - Name consistently. If a variable is logically the "same" across a function, don't create a second one with the same name.
- Package-level variables are fine for shared state, config, or precomputed values. Don't overuse them, but don't fear them.
Check your understanding
Practice exercises
Find the shadow
Read the following snippet carefully. What does it print, and why?
package main
import "fmt"
func main() {
n := 1
for i := 0; i < 3; i++ {
n := n + i
fmt.Println(n)
}
fmt.Println("final:", n)
}
Show the answer
Output:
1
2
3
final: 1
Inside the loop, n := n + i declares a new n
scoped to the loop body (shadowing the outer n). Each
iteration computes outer_n + i. Outside the loop,
n is the unchanged 1.
Fix: if you wanted the outer n to accumulate, write n = n + i (or n += i).
Further reading
Scope handled, no more silent variable swaps.