Introduction
Choosing receiver type in Go is one of the most important API design decisions for a struct. It affects behavior, interface compliance, performance characteristics, and code clarity.
Many explanations stop at “pointer can mutate, value cannot.” That is true but incomplete. The deeper topic is method sets and how Go decides whether a type satisfies an interface.
Quick Definitions
Value receiver:
func (p Part) Name() string { ... }
Pointer receiver:
func (p *Part) Rename(newName string) { ... }
Behavior Difference
Value receiver
- Method gets a copy of receiver.
- Changes inside method do not mutate original value.
- Good for small immutable-like structs.
Pointer receiver
- Method gets pointer to original value.
- Method can mutate original.
- Avoids copying large structs.
Method Set Rules (Critical)
This is the part most people miss.
For type T:
- Method set of
Tincludes methods with receiverTonly. - Method set of
*Tincludes methods with receiverTand*T.
Implication: if an interface requires a method defined on *T, then only *T satisfies that interface, not T.
Interface Example
type Renamer interface {
Rename(string)
}
type Part struct {
Name string
}
func (p *Part) Rename(s string) {
p.Name = s
}
Valid:
var r Renamer = &Part{Name: "A"}
Invalid:
// var r Renamer = Part{Name: "A"} // compile error
Because Rename is only on *Part method set.
Auto Addressing and Its Limits
Go lets you call pointer receiver methods on addressable values:
var p Part
p.Rename("x") // compiler uses (&p).Rename("x")
But this convenience does not change interface method set rules.
Also, auto-addressing does not work in all situations, such as non-addressable values from map indexing.
Common Map Pitfall
type Counter struct { N int }
func (c *Counter) Inc() { c.N++ }
m := map[string]Counter{"a": {N: 1}}
// m["a"].Inc() // compile error: map element not addressable
Fix by storing pointers:
m := map[string]*Counter{"a": {N: 1}}
m["a"].Inc()
Performance Considerations
Pointer receivers can reduce copying for large structs, but do not assume pointer is always faster. It depends on:
- Struct size.
- Escape analysis.
- CPU cache locality.
- Allocation patterns.
For small structs, value receivers can be perfectly efficient and simpler.
Use benchmarks for critical paths:
go test -bench . -benchmem
Concurrency and Safety
Pointer receivers increase shared mutable state risk.
If multiple goroutines access same pointer receiver state, synchronization is needed:
- mutex.
- channels.
- copy-on-write patterns.
Value receiver APIs can reduce accidental shared state in some designs.
Idiomatic Receiver Conventions
Practical convention used by many Go teams:
- If any method needs pointer receiver, make all methods pointer receiver for consistency.
- Use value receivers for small, immutable value objects.
- Avoid mixing receiver types unless there is clear reason.
Mixed receivers can confuse users and interface behavior.
Example with Both Kinds of Methods
package main
import (
"fmt"
"strings"
)
type Part struct {
ID int
Name string
}
// Pointer receiver: mutates state
func (p *Part) UpperCase() {
p.Name = strings.ToUpper(p.Name)
}
// Value receiver: read-only semantic
func (p Part) HasPrefix(prefix string) bool {
return strings.HasPrefix(p.Name, prefix)
}
func main() {
p := Part{ID: 1, Name: "widget"}
p.UpperCase()
fmt.Println(p.Name) // WIDGET
fmt.Println(p.HasPrefix("W")) // true
}
Decision Checklist
Use pointer receiver when:
- Method must mutate receiver.
- Receiver struct is large.
- Type contains sync primitives (
sync.Mutexshould not be copied). - Interface needs pointer methods.
Use value receiver when:
- Type behaves like immutable value.
- Struct is small and cheap to copy.
- You want clearer non-mutating semantics.
Practical Anti-Patterns
- Mixing pointer/value receivers without documented reason.
- Copying structs that embed mutexes.
- Assuming auto-addressing means interface compliance.
- Ignoring benchmark evidence in performance-sensitive code.
Conclusion
Receiver choice is API design. It affects semantics, interface satisfaction, and long-term maintainability.
When in doubt, optimize for clarity first, then validate performance with benchmarks. For mutable types and larger structs, pointer receivers are usually the safer default.
Resources
- Effective Go: Pointers vs Values
- Go Specification: Method sets
- Go FAQ: Why different method sets
- Go Blog
Comments