Value Receivers — Optimize¶
1. Type size¶
| Size | Choice | Reason |
|---|---|---|
| ≤ 16 bytes | Value | Passed in registers, copy is very fast |
| 16–64 bytes | Depends | Justify with a profile |
| > 64 bytes | Pointer | Exceeds a cache line, copy is expensive |
2. Field order (padding)¶
// BAD — 24 bytes
type Bad struct {
a bool // 1 + 7 padding
b int64 // 8
c bool // 1 + 7 padding
}
// GOOD — 16 bytes
type Good struct {
b int64 // 8
a bool // 1
c bool // 1 + 6 padding
}
The fieldalignment tool shows the optimized order.
3. Inline candidates¶
Small value receiver methods are good inline candidates:
go build -gcflags='-m' shows: "can inline (Point).X".
4. Defensive copy — cost¶
func (b Box) Items() []int {
out := make([]int, len(b.items)) // alloc
copy(out, b.items) // copy
return out
}
Defensive copy can be expensive. Only do it when mutation of the original is a real risk.
5. Slice header optimization¶
Slice value receiver — the slice header (24 bytes) is copied. This is usually cheap.
6. Pure function inline → no escape¶
func (p Point) DistSq() int { return p.x*p.x + p.y*p.y }
p := Point{3, 4}
result := p.DistSq() // inlined — p stays on the stack, no escape
7. Interface escape¶
type S struct{ name string }
func (s S) String() string { return s.name }
s := S{name: "x"}
var i fmt.Stringer = s // s escapes to the heap
The contents of the interface value go to the heap. Sometimes the value escapes at the caller.
8. Comparable for map keys¶
A comparable type as a map key — the hash is cheap and consistent:
9. Sync.Pool with value types¶
When the value type is large, reuse it via sync.Pool:
var pool = sync.Pool{New: func() any { return new(BigStruct) }}
func process() {
b := pool.Get().(*BigStruct)
defer pool.Put(b)
// ...
}
But this uses pointers — value receivers do not fit.
10. Profile¶
Memory:
11. Cleaner code¶
Pure logic — value receiver¶
Easy to test, immutable, parallel-safe.
Avoid hidden mutations¶
// BAD — returns the slice field
func (b Box) Items() []int { return b.items }
// GOOD — defensive or duplicated
func (b Box) Items() []int {
out := make([]int, len(b.items))
copy(out, b.items)
return out
}
Constructor validation¶
func NewEmail(s string) (Email, error) {
if !valid(s) { return Email{}, errInvalid }
return Email{value: s}, nil
}
An invalid value cannot be created.
12. Cheat Sheet¶
TYPE SIZE
─────────────────────────
≤16 → value (register)
16-64 → with a profile
>64 → pointer (cache line)
FIELD ORDER
─────────────────────────
Largest to smallest (minimum padding)
fieldalignment tool
INLINE
─────────────────────────
Small body preferred
defer/recover/goroutine — no inline
ESCAPE
─────────────────────────
Stack: local value, return value
Heap: interface, goroutine
DEFENSIVE COPY
─────────────────────────
Slice/map field → mutation risk
out := make(...); copy(out, ...)
CLEANER CODE
─────────────────────────
Pure logic → value
Don't mutate — return new
Constructor validation
13. Summary¶
Value receiver performance: - Small type → fast - Padding optimization — smaller - Good for inlining - Comparable → map key, == works - Pure function — test/concurrency are cheap
Cleaner code: - Immutable update (return new) - Defensive copy — slice/map fields - Constructor validation - Documentation — immutability disclaimer
Value receiver — one of Go's simplest, most powerful tools. Used correctly, code becomes simpler, has fewer bugs, and concurrency concerns become tractable.