Pointer Receivers — Optimize¶
1. Rules for choosing the receiver¶
Type size
─────────────────
≤ 16 bytes → value (copying is faster for small types)
> 16 bytes → pointer
has sync → pointer (always)
2. Small type — value receiver is faster¶
type Point struct{ X, Y int } // 16 bytes
// Value — register move
func (p Point) DistSq() int { return p.X*p.X + p.Y*p.Y }
// Pointer — extra dereference
func (p *Point) DistSqP() int { return p.X*p.X + p.Y*p.Y }
BenchmarkPoint shows the value variant as faster in most cases.
3. Large type — pointer receiver¶
type Big struct{ data [1024]int } // 8KB
// Value — 8KB copy on every call
func (b Big) Sum() int { ... }
// Pointer — 8 bytes
func (b *Big) Sum() int { ... }
4. Inline opportunities¶
When a pointer receiver method is small — it's a good inline candidate:
Inline = no call overhead.
5. Escape avoidance¶
Method value escape¶
Solution¶
// Direct
for _, x := range data { obj.Process(x) }
// Or build it once
cb := obj.Process
for _, x := range data { cb(x) }
6. Atomic vs Mutex¶
Atomic (lock-free)¶
Speed: ~5-10ns
Mutex¶
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock(); defer c.mu.Unlock()
c.n++
}
Speed: ~20-30ns (no contention)
Atomic — typically 2-3x faster.
7. Choosing RWMutex¶
Read methods use RLock, write methods use Lock — for read-heavy workloads.
But: if writes are rare and reads are very frequent — sync.Map or atomic.Pointer[map] may be faster.
8. Sync.Pool — re-use¶
var bufPool = sync.Pool{New: func() any { return &Buffer{} }}
func process(data []byte) {
b := bufPool.Get().(*Buffer)
defer bufPool.Put(b)
b.Reset()
// use b
}
Reduces heap allocation.
9. Profile first¶
Premature optimization — root of evil. Justify with profiling.
10. Cleaner code patterns¶
Separate pure logic¶
// Pull out the pure logic inside
func (s *Service) Process(req Req) Resp {
validated := validate(req) // pure
enriched := enrich(validated, s.cfg) // pure
return s.persist(enriched) // method (state)
}
Receiver consistency¶
// Bad — mixed
func (c Cache) Len() int { ... }
func (c *Cache) Set(k, v string) { ... }
// Good
func (c *Cache) Len() int { ... }
func (c *Cache) Set(k, v string) { ... }
Constructor responsibility¶
// Let the constructor handle internal init the caller doesn't know about
func NewCache() *Cache {
return &Cache{
m: map[string]string{},
// mutex zero-value OK
}
}
11. Generics + pointer receiver¶
Generic monomorphization — one copy for pointer/interface types, separate ones for scalars. Confirm with profiling.
12. noCopy discipline¶
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
type SafeThing struct {
_ noCopy
mu sync.Mutex
// ...
}
go vet finds copy operations — prevents accidental misuse.
13. Cheat Sheet¶
RECEIVER CHOICE
─────────────────────────
≤16 bytes, immutable → value
>16 bytes or mutate → pointer
has sync primitive → pointer (mandatory)
hot path inline → small pointer receiver
ESCAPE CONTROL
─────────────────────────
go build -gcflags='-m=2'
method value (s.M) → likely escape
method expression (T.M) → no escape
CONCURRENCY
─────────────────────────
Atomic > Mutex > RWMutex > sync.Map
sync.Pool — reduce heap alloc
Lock-free design — atomic primitive
PROFILE
─────────────────────────
go test -bench=. -cpuprofile=cpu.prof
go test -bench=. -memprofile=mem.prof
go test -bench=. -trace=trace.out
go tool pprof / trace
14. Summary¶
Pointer receiver performance: - Small type → value, large type → pointer - Mutex/atomic → always pointer - Method value in a hot path → escape consequences - Atomic > Mutex in speed - noCopy marker prevents accidental copies - Justify with profiling, don't optimize prematurely
Cleaner code: - Separate pure logic into a function - Keep receiver style consistent - Constructor responsibility — internal init - Documentation — concurrency, lifecycle