Variables and Scope

Overview

Variables in Go store values that can change during program execution. Understanding declaration patterns and visibility rules (scope) is fundamental to writing correct Go programs.

Variable Declaration

Using var

// Explicit type
var name string
var age int

// With initialization
var name string = "Alice"
var age int = 30

// Type inference
var name = "Alice"  // Inferred as string
var age = 30        // Inferred as int

Short Variable Declaration (:=)

Inside functions, use := for concise declaration:

func main() {
    name := "Alice"   // var name = "Alice"
    age := 30         // var age = 30
    active := true    // var active = true
}

When to Use var vs :=

Use var Use :=
Package-level variables Inside functions
Explicit type needed Type can be inferred
Zero value is desired Initialization value provided
// Package level (var only)
var globalConfig Config

func main() {
    // Function level (either works, := preferred)
    count := 0

    // Explicit type needed
    var ratio float64 = 0  // := 0 would be int
}

Multiple Variables

// Same type
var x, y, z int

// Different values
var a, b, c = 1, "hello", true

// Short declaration
name, age, active := "Alice", 30, true

Variable Block

var (
    name   string = "Alice"
    age    int    = 30
    active bool   = true
)

Redeclaration and Shadowing

Redeclaration with :=

:= can redeclare if at least one variable is new:

x := 1
x, y := 2, 3  // x redeclared, y is new (allowed)

// This fails:
// x := 4  // Error: no new variables on left side

Variable Shadowing

An inner scope can declare a variable with the same name as an outer scope:

x := 1
fmt.Println(x)  // 1

if true {
    x := 2       // New x, shadows outer x
    fmt.Println(x)  // 2
}

fmt.Println(x)  // 1 (outer x unchanged)

Warning: Shadowing often creates bugs:

var err error

if condition {
    result, err := someFunc()  // err shadows! Outer err unchanged
    // ...
}

if err != nil {  // This checks outer err, always nil!
    // Never reached
}

Fix:

var err error
var result Result

if condition {
    result, err = someFunc()  // No :=, uses outer err
}

Scope Levels

Package Scope

Variables declared outside functions are visible to the entire package:

package mypackage

var packageVar = "visible to all files in mypackage"

func init() {
    packageVar = "can modify here"
}

func SomeFunc() {
    fmt.Println(packageVar)  // Accessible
}

File Scope (Imports)

Import names are scoped to the file:

// file1.go
import "fmt"  // fmt available only in this file

// file2.go
import "fmt"  // Must import again

Function Scope

Parameters and local variables are visible within the function:

func greet(name string) {
    message := "Hello, " + name  // Local to greet
    fmt.Println(message)
}

// name and message are not accessible here

Block Scope

Variables declared in blocks ({}) are visible only within that block:

if x > 0 {
    y := x * 2  // Only visible inside if block
    fmt.Println(y)
}
// y is not accessible here

for i := 0; i < 10; i++ {
    // i is visible only in the loop
}
// i is not accessible here

switch n := getValue(); n {
case 1:
    // n is visible in switch block
}
// n is not accessible here

Visibility (Exported vs Unexported)

Go uses capitalization for visibility across packages:

package user

var PublicVar = "accessible from other packages"   // Exported
var privateVar = "only in user package"            // Unexported

func PublicFunc() {}   // Exported
func privateFunc() {}  // Unexported

type PublicType struct {
    PublicField  string  // Exported
    privateField string  // Unexported
}

The Blank Identifier (_)

Use _ to discard unwanted values:

// Discard second return value
value, _ := someFunc()

// Discard loop index
for _, item := range items {
    process(item)
}

// Import for side effects only
import _ "github.com/lib/pq"

Variable Lifetime

Stack vs Heap

Go decides where to allocate based on escape analysis:

func createValue() int {
    x := 42  // Likely on stack
    return x // Copy returned, x can be reclaimed
}

func createPointer() *int {
    x := 42   // Must go on heap
    return &x // Pointer escapes function
}

Garbage Collection

You don’t need to free memory—Go’s garbage collector handles it:

func process() {
    data := make([]byte, 1024*1024)  // 1MB allocated
    // Use data...
}  // data becomes garbage, will be collected

Common Patterns

Zero Value Initialization

var config Config  // All fields are zero values
config.Timeout = 30 * time.Second  // Set only what differs

Conditional Initialization

var s string
if condition {
    s = "yes"
} else {
    s = "no"
}

// Or with short declaration
s := "default"
if condition {
    s = "override"
}

Multiple Assignment

// Swap without temp variable
a, b = b, a

// Multiple returns
x, y, z := multiReturn()

Common Pitfalls

Accidental Shadowing

// Bug: err is shadowed
var err error
if x > 0 {
    y, err := compute()  // New err!
    _ = y
}
// Original err still nil

// Fix: declare y separately
var err error
var y int
if x > 0 {
    y, err = compute()  // Uses outer err
}

Loop Variable Capture

// Bug: all goroutines see last value
for _, v := range values {
    go func() {
        fmt.Println(v)  // v is shared!
    }()
}

// Fix: pass as parameter
for _, v := range values {
    go func(v int) {
        fmt.Println(v)
    }(v)
}

// Go 1.22+ fixes this by default

Summary

Declaration Usage
var x T Zero value, explicit type
var x = value Type inference
x := value Short declaration (functions only)
var x, y T Multiple same type
x, y := a, b Multiple with inference
Scope Visibility
Package All files in package
File Imports
Function Function body
Block {} boundaries