Variables and Scope

Updated

September 13, 2026

Variables and Scope

A name is visible in the block that declared it, and in nested blocks, until a new declaration hides it. The boring default is: declare at the smallest block that needs the name, and treat := as a new variable, not an assignment.

Mental model

Go has three places a variable lives:

  • Packagevar or const at file top. Visible through the package.
  • Function — parameters and names declared in the function body.
  • Block — anything inside { ... }, including if, for, and switch.

= assigns to an existing name. := declares at least one new name in the current block. If that name already exists in an outer block, := still declares a new inner variable. That is shadowing. It compiles. It is the trap.

Worked examples

Case 1: Package, function, block

Save as three_scopes.go. Prints prove which name is live.

// three_scopes.go
package main

import "fmt"

var desk = "front"

func main() {
    fmt.Println("package", desk)

    shift := "Amina"
    fmt.Println("function", shift)

    {
        table := 4
        fmt.Println("block", desk, shift, table)
    }
}

Run:

go run three_scopes.go

Output:

package front
function Amina
block front Amina 4

table does not exist after the inner braces. desk and shift still do.

Case 2: Inner block ends, outer name remains

Save as block_ends.go. The inner n dies with its braces.

// block_ends.go
package main

import "fmt"

func main() {
    n := 1
    {
        n := 2
        fmt.Println("inner", n)
    }
    fmt.Println("outer", n)
}

Run:

go run block_ends.go

Output:

inner 2
outer 1

Two variables named n. The inner one is not an assignment to the outer one.

Case 3: := in if is a new block

Save as if_init.go. The short statement on if belongs to the if (and its else), not to the rest of main.

// if_init.go
package main

import "fmt"

func tableFor(name string) (int, bool) {
    if name == "window" {
        return 4, true
    }
    return 0, false
}

func main() {
    if n, ok := tableFor("window"); ok {
        fmt.Println("seated at", n)
    }
    if n, ok := tableFor("patio"); ok {
        fmt.Println("seated at", n)
    } else {
        fmt.Println("no table", n, ok)
    }
}

Run:

go run if_init.go

Output:

seated at 4
no table 0 false

n and ok in the else are the same inner pair from that if. They are gone on the next line of main. That is why you can write two if n, ok := ... in a row.

Case 4: := vs = in the same function

Save as assign_or_declare.go. After err exists, = updates it. A later := with a new name on the left still redeclares err if you are in a new block.

// assign_or_declare.go
package main

import (
    "fmt"
    "strconv"
)

func main() {
    n, err := strconv.Atoi("12")
    fmt.Println("first", n, err)

    n, err = strconv.Atoi("x")
    fmt.Println("assign", n, err)

    if n, err := strconv.Atoi("4"); err == nil {
        fmt.Println("if", n, err)
    }
    fmt.Println("after if", n, err)
}

Run:

go run assign_or_declare.go

Output:

first 12 <nil>
assign 0 strconv.Atoi: parsing "x": invalid syntax
if 4 <nil>
after if 0 strconv.Atoi: parsing "x": invalid syntax

The if parsed "4" successfully. main’s n and err are still the failed "x" parse. Look at after if.

The trap

Short declaration in a nested block is the Monday-morning bug. You think you updated err. You declared a second err. Save as shadow_err.go:

// shadow_err.go
package main

import (
    "fmt"
    "strconv"
)

func main() {
    table, err := strconv.Atoi("4")
    if err != nil {
        fmt.Println("bad table")
        return
    }

    if cents, err := strconv.Atoi("not-a-price"); err != nil {
        fmt.Println("inner saw", err)
    } else {
        fmt.Println("price", cents)
    }

    if err != nil {
        fmt.Println("outer still set:", err)
        return
    }
    fmt.Println("open table", table)
}

Run:

go run shadow_err.go

Output:

inner saw strconv.Atoi: parsing "not-a-price": invalid syntax
open table 4

The inner err is not main’s err. The outer if err != nil is false, so the desk “opens” after a failed price parse. The fix: use = when the name already exists in this block (cents, err = strconv.Atoi(...) after declaring cents), or return when the inner call fails without introducing a new err.

The boring rule

  • Smallest block that needs the name.
  • := means “new variable in this block.” = means “update.”
  • Never := an err you already have in the same function unless you are sure you want a second one.
  • if v := ...; cond keeps v out of the rest of the function — that is usually what you want.
  • Package-level var for process-wide state only. Prefer passing values.
  • If a print would surprise you, the name is shadowed. Print it.

Try this

  1. In block_ends.go, change the inner line to n = 2 (assignment). Both prints should show 2.
  2. In if_init.go, try fmt.Println(n) after the second if. It must not compile.
  3. In shadow_err.go, declare var cents int before the inner if and use cents, err = strconv.Atoi(...) so the failed parse is visible to the outer if.
  4. Add a package-level var shift = "desk" and a shift := "Bo" in main. Print both by moving the inner name (s := "Bo") so you can still see the package value.