Value Receivers vs Pointer Receivers

Understand the key differences between value and pointer receivers in Go methods, when to use each, and the common pitfalls that arise from the choice.

A method in Go declares a receiver parameter that sits between the func keyword and the method name. That receiver can be a plain value of the type, or a pointer to it. The choice is not cosmetic — it controls whether the method works on a copy of the caller’s data or on the original data itself. Once you understand the mechanics, the trade‑offs and the few hard rules, the decision becomes straightforward.

The Fundamental Distinction

When you define a method with a value receiver (e.g. func (t Type) Method()), Go passes a copy of the struct — or of the underlying value — to the method. Inside the method, you are reading or mutating that copy. The original caller’s value is untouched.

When you define a method with a pointer receiver (func (t *Type) Method()), Go passes the address of the caller’s value. Dereferencing the pointer inside the method gives you direct access to the original memory. Any modification is visible to the caller.

A short, concrete example makes the difference impossible to misinterpret:

package main
import "fmt"
type Counter struct {
    Value int
}
// Value receiver – works on a copy.
func (c Counter) IncrementCopy() {
    c.Value++
}
// Pointer receiver – works on the original.
func (c *Counter) IncrementReal() {
    c.Value++
}
func main() {
    c := Counter{Value: 10}
    c.IncrementCopy()
    fmt.Println(c.Value) // 10 (unchanged)
    c.IncrementReal()
    fmt.Println(c.Value) // 11
}

IncrementCopy receives a copy of the Counter struct, increments the copy’s field, and then the copy is discarded. The original c in main never changes. IncrementReal receives a pointer to c, dereferences it, and increments the actual field.

Silent No-Op:

A value receiver that appears to modify the struct will compile and run without error, but the changes vanish when the method returns. If you expect a mutation to persist, this is one of the most common bugs new Go developers hit.

Why Pointer Receivers Exist

The pointer receiver solves two practical problems that a value receiver cannot address:

  1. Mutating the caller’s value. Many methods need to update the fields of the struct they belong to. A value receiver cannot do this because it only ever sees a temporary copy.
  2. Avoiding the cost of a deep copy on every call. When a struct is large — dozens of fields, slices underneath, or contains arrays — copying the entire thing for a read‑only operation wastes CPU and memory. A pointer receiver passes an 8‑byte address regardless of struct size.

These two reasons are stated directly in the official Go tour and in the Go code review comments. They are the starting point, not the full decision matrix.

When to Use a Pointer Receiver

Several clear rules have emerged from the Go community and the standard library. Each one is a strong signal that a pointer receiver is the right choice.

The method must modify the receiver. If the purpose of the method is to update a field, append to a slice that the type wraps, or change any part of the value, the receiver must be a pointer. There is no alternative that works with a value receiver.

The struct contains a synchronisation field. If your type includes a sync.Mutex, sync.RWMutex, or similar, it must never be copied. A value receiver would copy the mutex, breaking its guarantee of mutual exclusion. The standard library enforces this by documenting that a Mutex must not be copied; a pointer receiver avoids the copy.

Copying a Mutex Is Undefined Behaviour:

Go’s go vet will flag a struct containing a sync.Mutex that is passed by value. Always use a pointer receiver for any type that embeds or owns a synchronisation primitive.

The struct is large. “Large” is intentionally vague, but a useful mental model: imagine passing every field of the struct individually as arguments to a function. If that feels excessive, use a pointer receiver. In practice, a struct with more than a handful of fields — or a field that contains a large array — is a good candidate.

Consistency within the type. If any method on the type needs a pointer receiver, all other methods should also use a pointer receiver. This keeps the method set uniform and prevents surprises when a method that does not need to mutate accidentally breaks interface satisfaction (more on that later). The Go FAQ explicitly recommends this approach.

The method reassigns the underlying slice. For a named slice type, if the method performs an append that may grow the backing array, or reassigns the slice header, it must use a pointer receiver. Otherwise, the new header is lost.

type IntStack []int
func (s *IntStack) Push(v int) {
    *s = append(*s, v)
}

Without the pointer receiver, the Push would append to the caller’s copy of the slice header, not the original slice header.

When Value Receivers Make Sense

Using a pointer receiver everywhere is possible but not always the best design. Value receivers have their own clear advantages.

Immutability by design. A value receiver communicates that the method will not alter the receiver. When you read func (t Thing) Describe(), you know that Describe reads data but does not change it. This is valuable information for anyone reading the code, and the compiler enforces it.

Small, value‑like types. If the type is naturally a value — like time.Time, a 2D point, or a wrapper around an int — copying it is cheap and semantically correct. These types are often used without pointers anyway, so a value receiver fits the mental model.

Concurrency safety. Value receivers operate on a local copy, so no other goroutine can interfere with that copy. Pointer receivers, by contrast, expose the original value to concurrent access. A value receiver is inherently safe to call from multiple goroutines simultaneously, provided the struct itself contains no pointer fields that alias shared data. If the type is small and immutable from the outside perspective, a value receiver is a natural concurrency safety net.

Escape analysis advantage. The compiler works hard to keep short‑lived values on the stack. A value receiver can often avoid heap allocation entirely because the copy is local to the stack frame. A pointer receiver may force the compiler to allocate the receiver on the heap if the pointer escapes the method. Fewer heap allocations mean less pressure on the garbage collector. However, as the Go code review comments warn: do not choose a value receiver for performance reasons without profiling first.

Correct use of immutability:

If your type represents an immutable data structure — like a configuration object that never changes after creation — value receivers are the idiomatic choice. The method's signature makes the intent self‑documenting.

How Method Invocation Handles the Receiver

Go allows a method with a value receiver to be called on both a value and a pointer to that value. The same holds for a pointer receiver — it can be called on a pointer or on an addressable value. The compiler transparently inserts the necessary address‑of (&) or dereference (*) operation.

type Box struct{ Label string }
func (b Box) Show() string    { return b.Label }
func (b *Box) SetLabel(s string) { b.Label = s }
func main() {
    b := Box{Label: "start"}
    // Value receiver called on value.
    fmt.Println(b.Show()) // ok
    // Value receiver called on pointer.
    bp := &b
    fmt.Println(bp.Show()) // compiler does (*bp).Show()
    // Pointer receiver called on pointer.
    bp.SetLabel("changed")
    fmt.Println(b.Label) // "changed"
    // Pointer receiver called on addressable value.
    b.SetLabel("again") // compiler does (&b).SetLabel(...)
    fmt.Println(b.Label) // "again"
}

The convenience is absolute: a caller never needs to think about whether they hold a value or a pointer when calling a method. However, this convenience has a boundary: if the value is not addressable (e.g., a return value captured directly), you cannot call a pointer receiver method on it. This is rare in everyday code, but it matters when a value is a temporary returned from a function.

Impact on Method Sets and Interfaces

The choice of receiver directly determines which methods belong to the method set of a type. The method set, in turn, controls whether a type implements an interface.

  • The method set of a value T contains all methods with value receiver (t T).
  • The method set of a pointer *T contains all methods with value receiver (t T) and all methods with pointer receiver (t *T).

A concrete outcome: if you define a method on *T that is required by an interface, then only *T satisfies that interface. A plain T will not, because its method set does not include pointer‑receiver methods.

type Greeter interface {
    Greet() string
}
type Person struct{ Name string }
func (p *Person) Greet() string { return "Hello, " + p.Name }
func main() {
    var g Greeter
    p := Person{Name: "Alice"}
    // g = p       // compile error: Person does not implement Greeter
    g = &p         // ok: *Person implements Greeter
    fmt.Println(g.Greet())
}

This is one reason the Go FAQ recommends consistency: if you mix value and pointer receivers on the same type, you create a situation where a value and a pointer to the same type have different interface‑satisfying capabilities. Keeping all receivers of a type the same avoids this confusion.

Method sets determine interface satisfaction:

The rule is simple but unintuitive at first: a pointer type's method set is a superset of the value type's method set. When you see a compile‑time error about missing methods, check whether the receiver type matches what you're passing.

Common Mistakes That Produce Bugs

Many of the tricky bugs around receivers come from a handful of predictable misunderstandings.

Modifying a value receiver expecting the change to stick. Already demonstrated above; this is the single most frequent mistake. The code compiles, runs, and produces a result that looks correct until you inspect the original value.

Copying a struct with a mutex inside. This often happens indirectly: a method takes a value receiver on a struct that contains a sync.Mutex. The compiler copies the mutex into the method’s frame, creating an independent lock that has no relationship to the original. The result is a silent breakdown of mutual exclusion. The go vet tool will warn about this pattern.

Appending to a slice without reassigning through a pointer. If you define a method on a named slice type and use a value receiver, an append inside the method can change the copy’s header — but the caller’s header is unchanged. If the append causes a new backing array allocation, the original slice does not see the new data at all.

Believing that value receivers are always slower. For tiny structs, the copy is trivially cheap and may even be elided by the compiler. The pointer‑receiver version may add an allocation that the value‑receiver version avoids. Benchmark before concluding.

Mixing receiver types arbitrarily. If some methods of a type require a pointer receiver but others use value receivers, the method set becomes asymmetrical. A value of that type satisfies a subset of the methods that the pointer satisfies, leading to interface surprises.

Performance Considerations

The performance difference between the two receivers is easy to overstate. Copying a struct of a few words is comparable to passing a pointer (which is also a copy — a copy of the address). The cost curve steepens as the struct grows: copying a struct with ten large strings or a few slices will be measurably more expensive than copying a pointer.

Pointer receivers can introduce heap allocations if the pointer escapes the method. The Go compiler’s escape analysis decides whether the receiver can stay on the stack. A value receiver often avoids this entirely, because the copy lives on the stack and never needs to escape. This sometimes makes value receivers faster for small, frequently called methods.

The canonical advice is: let correctness and semantics drive the choice first, and only reach for performance tuning after profiling reveals a real bottleneck.

Decision Framework

When you stand in front of a new type and need to pick a receiver, the following sequence of questions will land you on the right choice almost every time.

  1. Does the method need to mutate the receiver? Yes → pointer receiver.
  2. Does the type contain a sync.Mutex or similar? Yes → pointer receiver.
  3. Is the struct very large (roughly: would passing all fields as arguments feel heavy)? Yes → pointer receiver.
  4. Do any of the type’s methods already use a pointer receiver? Yes → use pointer receiver for consistency.
  5. Is the type a small, immutable value like a point, a time, or a primitive wrapper? Yes → value receiver.
  6. For all other cases, a pointer receiver is the safer default. It keeps the method set uniform and avoids accidental copy‑only mutations.

Your choice validated:

If you’ve answered the questions above and the code compiles, runs, and passes go vet, your receiver choice is likely correct. The Go toolchain’s static analysis and the language’s strong type system are your allies here.

Summary

Value and pointer receivers are two sides of the same coin. Value receivers work on a local copy and guarantee immutability; pointer receivers grant direct access to the original value. The rules that guide the choice are practical and grounded in real‑world trade‑offs: mutation, copying cost, method‑set consistency, and the presence of synchronisation primitives all pull the decision in a clear direction.

The single most important takeaway is to be intentional. Write the receiver that reflects what the method truly does. When the intent is mutation, use a pointer. When the intent is read‑only and the type is light, a value receiver is both safe and expressive. And when in doubt, favour pointer receivers — they keep the method set consistent and prevent the hardest‑to‑notice bugs.