CHAPTER 17 · ORGANIZING CODE

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 -vettool or shadow to catch shadowing.

Scope levels

Top to bottom, narrowest at the bottom:

  1. Universe: built-ins like int, len, nil, true.
  2. Package: any name declared at file level, in any file of the package.
  3. File: imported package names.
  4. Function: parameters and results.
  5. Block: any { ... } including the body of if, 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 { ... } keeps err out 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

EXERCISE 1

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.