Back to blog

Saturday, August 15, 2026

Go Pointer vs Value Receiver: Which Should You Use?

Go Pointer vs Value Receiver: Which Should You Use?

Go Pointer vs Value Receiver: Which Should You Use?

Use a pointer receiver when the method must change the value, when the type contains a sync.Mutex (or anything that must not be copied), or when the struct is large enough that copying it on every call is wasteful. Use a value receiver for small, copy-cheap types that you treat as data (time.Time, a 2D point). Once any method on the type needs a pointer, give all methods pointer receivers.

The Go Tour names two reasons for a pointer: mutation, and avoiding a copy of a large value. Everything else is a consequence of those two plus method sets.

The failure

A value receiver looks like a setter. It is not.

package main

import "fmt"

type Counter struct {
    N int
}

func (c Counter) Inc() { c.N++ }

func main() {
    c := Counter{N: 0}
    c.Inc()
    fmt.Println(c.N) // 0
}

The compiler is happy. Inc incremented a copy and threw it away.

The other silent failure: mixing receivers, then assigning a value to an interface.

type Setter interface {
    Set(int)
}

func (c *Counter) Set(v int) { c.N = v }

func main() {
    var s Setter
    c := Counter{}
    // s = c  // compile error: Counter does not implement Setter
    s = &c    // ok
}

T's method set is value-receiver methods only. *T's method set is both. A pointer-receiver method is not on the value.

Why this happens

A method is a function whose first argument is the receiver. func (c Counter) Inc() is Inc(c Counter). Go copies c. func (c *Counter) Inc() copies the pointer, which still names the same Counter.

Go will insert & or * for you when the value is addressable: c.Inc() on a variable becomes (&c).Inc(). It will not take the address of a map index, a function return, or any other temporary. That is why m["x"].Inc() fails if Inc has a pointer receiver, and why makeCounter().Inc() fails the same way.

Copying a struct that holds a sync.Mutex copies the mutex. Each call then locks a different mutex. go vet flags this. Pointer receivers avoid the copy.

Pointer receivers can force the value onto the heap (escape analysis). For a tiny struct, a value receiver is often cheaper. Do not pick the receiver for speed until a benchmark says it matters.

The decision

Walk the list in order. Stop at the first yes.

  1. Does the method mutate the receiver, reassign a slice header, or append in a way that must stick? → pointer.
  2. Does the type contain a mutex or other non-copyable field? → pointer on every method.
  3. Is the struct large (imagine passing every field as a separate argument)? → pointer.
  4. Does any other method on this type already use a pointer? → pointer, for a uniform method set.
  5. Is the type a small immutable value? → value.
  6. Otherwise default to pointer. It is the choice that does not silently no-op a later setter.
func (c *Counter) Inc() { c.N++ }

func (c *Counter) Value() int { return c.N } // pointer too, for consistency

For a value object, keep it a value:

type Point struct{ X, Y float64 }

func (p Point) Translate(dx, dy float64) Point {
    return Point{X: p.X + dx, Y: p.Y + dy}
}