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 intShort 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, trueVariable 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 sideVariable 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 againFunction 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 hereBlock 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 hereVisibility (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 collectedCommon Patterns
Zero Value Initialization
var config Config // All fields are zero values
config.Timeout = 30 * time.Second // Set only what differsConditional 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 defaultSummary
| 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 |