Pointers
Pointers hold memory addresses. In Go they enable shared mutation, large-struct passing without copies, and optional values for types that have no nil. The syntax is small — *T for the type, &v to take an address, *p to dereference.
Pointers, receivers, nil safety
EXAMPLE
package main
import "fmt"
// 1) Basic syntax
func main() {
x := 10
p := &x // p is *int — pointer to x
fmt.Println(p) // 0xc0000180a8 (some address)
fmt.Println(*p) // 10 — dereference
*p = 20 // write through the pointer
fmt.Println(x) // 20
}
// 2) Pointers as function parameters — mutation across call boundaries
func increment(n int) {
n++ // operates on the COPY
}
func incrementPtr(n *int) {
*n++ // operates on the original
}
func main2() {
x := 5
increment(x)
fmt.Println(x) // 5
incrementPtr(&x)
fmt.Println(x) // 6
}
// 3) Avoiding large-struct copies
type Order struct {
ID string
Items []LineItem
Total int
/* … many more fields … */
}
func total(o Order) int { return o.Total } // copies the struct on every call
func total2(o *Order) int { return o.Total } // 8 bytes on the stack, no copy
// Rule of thumb: pass by pointer when the struct is big OR you intend to mutate it.
// Pass by value when the struct is small and immutable.
// 4) Methods — value vs pointer receivers
type Counter struct{ n int }
func (c Counter) Read() int { return c.n } // value receiver — safe to call on Counter or *Counter
func (c *Counter) Inc() { c.n++ } // pointer receiver — needs an addressable value
func main3() {
var c Counter
c.Inc() // Go auto-takes &c
c.Inc()
fmt.Println(c.Read()) // 2
// Map values are NOT addressable
m := map[string]Counter{"a": {}}
// m["a"].Inc() // compile error
tmp := m["a"]; tmp.Inc(); m["a"] = tmp // workaround
// Or store *Counter values: map[string]*Counter
}
// 5) Mixing receivers — consistency rule
// If ANY method on a type uses a pointer receiver, use pointer receivers for ALL methods.
// Otherwise you get half-and-half behaviour and surprising no-ops on value copies.
// 6) new() — allocates a zero value, returns its pointer
func main4() {
p := new(int) // *int pointing at 0
*p = 42
fmt.Println(*p) // 42
type Config struct{ Host string; Port int }
cfg := new(Config) // *Config pointing at &Config{}
cfg.Host = "localhost"
cfg.Port = 8080
}
// In practice: &Config{Host: "x", Port: 8080} is more readable than new()+fields.
// 7) nil pointers and panics
func main5() {
var p *int // nil
if p != nil { fmt.Println(*p) } // safe — guarded
// fmt.Println(*p) // runtime panic — nil dereference
}
// 8) Optionality via pointer
type User struct {
Name string
Email string
AvatarURL *string // optional — nil means 'no avatar'
}
func renderAvatar(u User) string {
if u.AvatarURL == nil { return "<default>" }
return *u.AvatarURL
}
// Helper to take address of literal
func strp(s string) *string { return &s }
u := User{Name: "mara", AvatarURL: strp("https://…")}
// 9) Returning pointers — Go figures out escape analysis
func newCounter() *Counter {
c := Counter{n: 0}
return &c // c escapes to the heap; safe to return
}
// Unlike C, you can return the address of a local variable. The compiler moves it.
// 10) Slices and maps already contain references
type Slice = []int
// passing a slice by value still shares the underlying array
func fill(s []int) {
for i := range s { s[i] = i } // visible to caller
}
// Same for maps. Pointers to slices/maps are unusual; you only need them if you'll REPLACE the
// slice/map header itself (e.g. append in place).
func growAppend(s *[]int, v int) { *s = append(*s, v) }
// 11) Comparing pointers
// p == q → true if both point to the same address (identity, not value equality)
// p == nil → standard nil check
// 12) Atomic and concurrent — sync/atomic, sync.Mutex
import (
"sync"
"sync/atomic"
)
var hits atomic.Int64
hits.Add(1)
hits.Load()
var mu sync.Mutex
mu.Lock(); /* mutate shared state */ mu.Unlock()
// 13) Pointer to interface — almost always wrong
var w io.Writer
// var p *io.Writer = &w // works but is rarely useful; pass io.Writer directly
// 14) When NOT to use pointers
// • Small structs (< ~64 B) you don't mutate — pass by value, cache-friendly
// • Strings (already a small immutable header)
// • time.Time (small + immutable in practice)
// • Iterator-style transformations — return new value
// 15) Common bugs
// • Returning a pointer to a loop variable — captures the SAME var, not each iteration
// for _, item := range items {
// ps = append(ps, &item) // ❌ all entries point to the last value (pre-Go 1.22)
// }
// Fix: item := item inside the loop, or Go 1.22+ loop semantics
// • Calling pointer-receiver method on a map value — not addressable
// • Mixing value and pointer receivers on the same type — inconsistent behaviour
// • Comparing *T values expecting equality of CONTENTS — compares addresses
// • Storing pointers to short-lived stack values across goroutines without ownership rules
Why it matters
Use pointers to mutate, to avoid copying large structs, or to express optionality. Stick to one receiver style per type, and watch the loop-variable trap when you store addresses inside a range — pre-Go 1.22 captures the same variable, which has burned many of us at least once.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
x := 10 p := &x // address-of *p = 20 // dereference assign fmt.Println(x) // 20Try it Yourself »
Discussion
Loading…