Choosing between value and pointer receivers is one of the most consequential API decisions you make for a Go struct. It affects mutation semantics, interface satisfaction, memory allocation, and concurrency safety. Most explanations stop at “pointers can mutate, values can’t” — but the deeper story is method sets and how they interact with interfaces.
Quick Reference
// Value receiver — method gets a copy of the struct
func (p Point) Distance() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
// Pointer receiver — method gets a pointer to the original
func (p *Point) Scale(factor float64) {
p.X *= factor
p.Y *= factor
}
Behavior: What Each Receiver Gets
type Counter struct {
count int
}
// Value receiver: operates on a copy — cannot mutate original
func (c Counter) ValueGet() int {
return c.count
}
func (c Counter) ValueBrokenIncrement() {
c.count++ // Increments the COPY, original unchanged
}
// Pointer receiver: operates on original — can mutate
func (c *Counter) PointerIncrement() {
c.count++ // Increments the ORIGINAL
}
func main() {
c := Counter{count: 0}
c.ValueBrokenIncrement()
fmt.Println(c.count) // 0 — copy was incremented, not c
c.PointerIncrement()
fmt.Println(c.count) // 1 — original incremented
// Go auto-dereferences: c.PointerIncrement() is sugar for (&c).PointerIncrement()
// This only works on addressable values (variables, not map elements)
}
The Method Set Rules (Critical)
This is the part most explanations miss. In Go, the set of methods available to a type depends on whether you have a value or a pointer:
| Type | Methods available |
|---|---|
T |
Only methods with receiver T |
*T |
Methods with receiver T and *T |
A pointer type has a superset of the value type’s methods.
type Door struct{ locked bool }
func (d Door) IsLocked() bool { return d.locked } // value receiver
func (d *Door) Lock() { d.locked = true } // pointer receiver
func (d *Door) Unlock() { d.locked = false } // pointer receiver
func main() {
d := Door{} // value
pd := &Door{} // pointer
d.IsLocked() // OK: value receiver on value
pd.IsLocked() // OK: value receiver — *T has all T methods
pd.Lock() // OK: pointer receiver on pointer
d.Lock() // OK at call site: Go auto-takes address → (&d).Lock()
// BUT: see interface section below — this auto-addressing has limits
}
Interface Satisfaction: The Real Constraint
The method set rules have a direct consequence for interface implementation:
type Locker interface {
Lock()
Unlock()
}
type Door struct{ locked bool }
func (d *Door) Lock() { d.locked = true }
func (d *Door) Unlock() { d.locked = false }
func main() {
var l Locker
l = &Door{} // OK: *Door has Lock and Unlock
l.Lock()
// l = Door{} // COMPILE ERROR: Door does not implement Locker
// // (only *Door has Lock/Unlock methods)
}
Rule: If any method required by an interface uses a pointer receiver, only the pointer type satisfies that interface — not the value type.
// This compiles fine — IsLocked has value receiver
type ReadOnlyLocker interface {
IsLocked() bool
}
var rl ReadOnlyLocker = Door{} // OK: Door has IsLocked() bool
var rl2 ReadOnlyLocker = &Door{} // Also OK: *Door has all Door methods
The Interface Compliance Pattern
A compile-time check that your type implements an interface:
// Placed at package level — causes a compile error if Door doesn't implement Locker
var _ Locker = (*Door)(nil)
This is idiomatic Go — you get an early, clear error rather than a confusing “does not implement” at the call site.
Auto-Addressing and Its Limits
Go sometimes auto-takes the address of a variable to call a pointer receiver method:
d := Door{}
d.Lock() // Go silently converts to (&d).Lock()
But this only works on addressable values. Map elements are not addressable:
type Door struct{ locked bool }
func (d *Door) Lock() { d.locked = true }
m := map[string]Door{"front": {}}
// m["front"].Lock() // COMPILE ERROR: cannot take address of map index expression
// Fix 1: Store pointers in the map
m2 := map[string]*Door{"front": {}}
m2["front"].Lock() // OK
// Fix 2: Copy, modify, put back
d := m["front"]
d.Lock()
m["front"] = d
The same issue appears with returned values:
func getDoor() Door { return Door{} }
// getDoor().Lock() // COMPILE ERROR: cannot take address of getDoor()
// The return value is a temporary — not addressable
Mutation and Concurrency Safety
Pointer receivers increase the risk of data races because multiple callers share the same underlying memory:
type SafeCounter struct {
mu sync.Mutex
count int
}
// Must use pointer receiver — mutex cannot be copied
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// WRONG: value receiver copies the mutex — creates independent lock
func (c SafeCounter) BadIncrement() {
c.mu.Lock() // locks a COPY of the mutex, not the original
defer c.mu.Unlock()
c.count++
}
Rule: If a struct contains a mutex or other sync primitive (sync.Mutex, sync.RWMutex, sync.WaitGroup), always use pointer receivers — value receivers would copy the sync primitive, breaking its synchronization guarantee.
Memory: Escape Analysis and Allocation
Pointer receivers don’t always reduce allocations. The Go compiler’s escape analysis determines whether a value lives on the stack or heap:
// This might escape to heap (accessed via pointer, survives function)
func newCounter() *Counter {
c := Counter{} // may escape to heap
return &c
}
// This stays on stack (value is copied out, Counter doesn't escape)
func computeDistance(p Point) float64 {
return p.Distance() // value receiver — no escape
}
For small structs (≤ 2 words / 16 bytes on 64-bit), value receivers are often faster because:
- No pointer indirection
- Better cache locality
- May avoid heap allocation entirely
For large structs, pointer receivers avoid copying:
// Large struct: 1000 bytes — value receiver copies 1000 bytes per call
type LargeConfig struct {
Fields [125]int64 // 1000 bytes
}
func (c LargeConfig) ExpensiveCopy() { /* ... */ } // copies 1000 bytes
func (c *LargeConfig) CheapPointer() { /* ... */ } // copies 8 bytes (pointer)
To measure, use benchmarks:
go test -bench=. -benchmem -count=3
Practical Conventions
The Go community has converged on these rules (from Effective Go and the standard library):
Use pointer receiver when:
- The method needs to mutate the receiver
- The struct contains a sync primitive (mutex, cond, etc.)
- The struct is large (>4 words / 32 bytes) and copying is expensive
- The type is used as an interface and some methods are pointer receivers — be consistent
Use value receiver when:
- The type is a small, immutable value type (think
time.Time,net.IP) - The method only reads the receiver
- The type is a basic type or array with comparable behavior
Consistency rule (most important): if any method on a type uses a pointer receiver, use pointer receivers for all methods of that type. Mixed receivers confuse users and create interface compliance surprises.
// BAD: Mixed receivers — confusing
func (t Thing) Name() string { return t.name } // value
func (t *Thing) SetName(s string) { t.name = s } // pointer
// GOOD: Consistent pointer receivers
func (t *Thing) Name() string { return t.name }
func (t *Thing) SetName(s string) { t.name = s }
Real-World Examples
HTTP Handler Pattern
// Handler struct with pointer receiver — common pattern
type UserHandler struct {
db *sql.DB
log *slog.Logger
}
// All methods use pointer receiver for consistency
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
users, err := h.getUsers(r.Context())
if err != nil {
h.log.Error("get users failed", slog.Any("error", err))
http.Error(w, "internal error", 500)
return
}
json.NewEncoder(w).Encode(users)
}
func (h *UserHandler) getUsers(ctx context.Context) ([]User, error) {
// ...
return nil, nil
}
Value Type (Immutable Semantics)
// Point behaves like a value — value receivers make sense
type Point struct{ X, Y float64 }
func (p Point) Distance(q Point) float64 {
dx, dy := p.X-q.X, p.Y-q.Y
return math.Sqrt(dx*dx + dy*dy)
}
func (p Point) Add(q Point) Point {
return Point{p.X + q.X, p.Y + q.Y}
}
func (p Point) Scale(factor float64) Point {
return Point{p.X * factor, p.Y * factor}
}
// Usage: immutable style — original is never modified
p := Point{3, 4}
q := p.Scale(2) // returns new Point, p unchanged
Builder Pattern (Pointer Chaining)
type QueryBuilder struct {
table string
where []string
limit int
}
func (q *QueryBuilder) Table(t string) *QueryBuilder {
q.table = t
return q // return pointer for chaining
}
func (q *QueryBuilder) Where(condition string) *QueryBuilder {
q.where = append(q.where, condition)
return q
}
func (q *QueryBuilder) Limit(n int) *QueryBuilder {
q.limit = n
return q
}
func (q *QueryBuilder) Build() string {
query := "SELECT * FROM " + q.table
if len(q.where) > 0 {
query += " WHERE " + strings.Join(q.where, " AND ")
}
if q.limit > 0 {
query += fmt.Sprintf(" LIMIT %d", q.limit)
}
return query
}
// Usage
query := (&QueryBuilder{}).
Table("users").
Where("age > 18").
Where("active = true").
Limit(10).
Build()
Summary: Decision Checklist
Does the method mutate the receiver? → Pointer receiver Does the struct contain sync.Mutex/WaitGroup? → Pointer receiver (always) Is the struct large (>32 bytes)? → Pointer receiver Does any OTHER method use pointer receiver? → Pointer receiver (consistency) Is this a small immutable value type? → Value receiver Does the method only read? → Either (prefer consistency)
When in doubt: use pointer receivers. It’s easier to change from pointer to value later than vice versa, and pointer receivers give you more flexibility with interfaces.
Resources
- Effective Go: Pointers vs Values
- Go specification: Method sets
- Go FAQ: Different method sets for T and *T
- Go code review comments: Receiver Type
Comments