Skip to main content
โšก Calmops

Value vs Pointer Receivers in Go: Method Sets, Interfaces, and Performance

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

  1. Method gets a copy of receiver.
  2. Changes inside method do not mutate original value.
  3. Good for small immutable-like structs.

Pointer receiver

  1. Method gets pointer to original value.
  2. Method can mutate original.
  3. Avoids copying large structs.

Method Set Rules (Critical)

This is the part most people miss.

For type T:

  1. Method set of T includes methods with receiver T only.
  2. Method set of *T includes methods with receiver T and *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:

  1. Struct size.
  2. Escape analysis.
  3. CPU cache locality.
  4. 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:

  1. mutex.
  2. channels.
  3. 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:

  1. If any method needs pointer receiver, make all methods pointer receiver for consistency.
  2. Use value receivers for small, immutable value objects.
  3. 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:

  1. Method must mutate receiver.
  2. Receiver struct is large.
  3. Type contains sync primitives (sync.Mutex should not be copied).
  4. Interface needs pointer methods.

Use value receiver when:

  1. Type behaves like immutable value.
  2. Struct is small and cheap to copy.
  3. You want clearer non-mutating semantics.

Practical Anti-Patterns

  1. Mixing pointer/value receivers without documented reason.
  2. Copying structs that embed mutexes.
  3. Assuming auto-addressing means interface compliance.
  4. 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

Comments