A pointer holds a memory address — it points to where a value lives. Pointers enable sharing a single value across multiple call frames (mutation through a function), building self-referential structures (linked lists, trees), and controlling whether values live on the stack or heap.
Go manages memory automatically with a garbage collector. You don’t call free — when no pointers reference a value, the GC reclaims it. But you still choose when to use pointers vs values, and that choice affects both correctness (can the function modify the original?) and performance (does copying matter?).
For pointer receivers on methods see Go function receivers and methods.
Address-Of and Dereference
&x returns the address of x (a pointer to x). *p dereferences a pointer — reads or writes the value it points to:
x := 42
p := &x // p is *int, holds x's address
fmt.Println(p) // 0xc0000b4010 (some address)
fmt.Println(*p) // 42 — dereference to read
*p = 100 // dereference to write
fmt.Println(x) // 100 — x was modified through p
Go automatically dereferences struct pointers when accessing fields — you can write p.Field instead of (*p).Field:
type Point struct{ X, Y int }
p := &Point{X: 3, Y: 4}
fmt.Println(p.X) // 3 — Go dereferences automatically
p.X = 10 // modifies the original Point
When to Use Pointers
Mutation through functions: if a function needs to modify a value owned by the caller, take a pointer:
// ❌ Value: counter is a copy — caller's counter is unchanged
func incrementCopy(c Counter) { c.n++ }
// ✅ Pointer: modifies the caller's Counter
func increment(c *Counter) { c.n++ }
Large structs: passing a 1KB struct by value copies 1KB per call. Pass a pointer (8 bytes on 64-bit) instead:
func processConfig(cfg *Config) error { ... } // avoids copying Config
Optional values: a pointer to a type is the idiomatic “optional” — nil means absent:
type User struct {
Name string
Email *string // nil if not provided
}
// Check before using
if u.Email != nil {
sendEmail(*u.Email)
}
Interface satisfaction: if any method requires a pointer receiver, the type only satisfies the interface through a pointer — see Go interfaces.
Nil Pointers
The zero value of a pointer is nil. Dereferencing a nil pointer panics:
var p *int // nil
fmt.Println(p) // <nil>
fmt.Println(*p) // panic: runtime error: invalid memory address or nil pointer dereference
Always check for nil before dereferencing if the pointer might be nil:
func getAge(p *Person) int {
if p == nil {
return 0 // safe default
}
return p.Age
}
The most common source of nil pointer panics in Go: forgetting that a struct field of pointer type is nil until explicitly set, or that a function returning (*T, error) returns nil, err on failure.
new vs make
new(T) allocates a zeroed T and returns a *T. It’s equivalent to var t T; return &t:
p := new(int) // *int pointing to 0
fmt.Println(*p) // 0
s := new(string) // *string pointing to ""
fmt.Println(*s) // ""
cfg := new(Config) // *Config with all zero fields
make(T, args...) only works for slices, maps, and channels — it initializes the internal structure that these types require:
sl := make([]int, 5) // initialized slice, len=5
m := make(map[string]int) // initialized map
ch := make(chan int, 10) // initialized buffered channel
You cannot make a struct or basic type — use new or just take the address of a composite literal:
// Equivalent ways to get *Config:
cfg1 := new(Config)
cfg2 := &Config{}
In practice, &Config{} is more common than new(Config) because it allows initializing fields in the same expression.
Stack vs Heap: Escape Analysis
Go decides whether a value lives on the stack (fast, automatically freed) or the heap (GC-managed) through escape analysis. The key rule: if a value’s address is used after the function returns, it must live on the heap:
// x escapes to heap — its address outlives the function
func newInt() *int {
x := 42
return &x // x must outlive newInt
}
// y stays on the stack — no pointer leaves the function
func sumSquares(a, b int) int {
y := a*a + b*b // y never escapes
return y
}
See which values escape:
go build -gcflags="-m" ./...
# main.go:3:2: moved to heap: x
Most escapes are intentional and correct. Excessive heap allocation increases GC pressure — profile with go test -benchmem to find hot allocation paths.
The Garbage Collector
Go’s GC is a concurrent, tricolor mark-and-sweep collector. Key properties:
- No manual free: when nothing points to a value, the GC reclaims it
- Stop-the-world pauses: sub-millisecond in modern Go
- Generational-ish: recent Go versions use arenas and compaction for short-lived objects
You can tune GC behavior:
// GOGC environment variable: GC target (default 100 = 100% heap growth before GC)
// Higher = less frequent GC, more memory used
// GOGC=off disables GC entirely (useful for batch jobs)
os.Setenv("GOGC", "200")
// Or in code:
debug.SetGCPercent(200)
// Force a GC cycle (useful before memory profiling)
runtime.GC()
For most services, the default GC settings are fine. If you’re seeing GC pauses in profiles (go tool pprof), investigate allocation patterns first rather than tuning GOGC.
Pointer Pitfalls
Loop variable capture in closures:
// ❌ All closures capture the same p variable
var ptrs []*int
for i := 0; i < 3; i++ {
ptrs = append(ptrs, &i) // all point to same i
}
// After loop: *ptrs[0] = *ptrs[1] = *ptrs[2] = 3
// ✅ Create a new variable per iteration
for i := 0; i < 3; i++ {
n := i
ptrs = append(ptrs, &n)
}
Storing pointers in maps invalidates slice element pointers:
users := []User{{Name: "Alice"}, {Name: "Bob"}}
p := &users[0]
users = append(users, User{Name: "Charlie"}) // may reallocate
// p may now point to old memory — use users[0] directly
If you need stable pointers to slice elements, use a slice of pointers ([]*User) instead of a slice of values.
Summary
&xgives a pointer to x;*preads or writes through a pointer; Go auto-dereferences struct field access- Use pointers for mutation across function boundaries, large structs, optional values, and interface satisfaction
- Always check for nil before dereferencing — nil pointer panics are the most common Go runtime panic
new(T)returns*Twith zero value;makeinitializes slices, maps, channels — not structs- Escape analysis decides stack vs heap; address-of causes heap allocation when the address outlives the function
- The GC manages heap memory automatically; tune GOGC only after profiling confirms GC pauses
Comments