Pointer Receivers vs Value Receivers
Learn how to choose between pointer and value receivers in Go methods, understand addressability rules, and avoid common pitfalls.
A method in Go is just a function with a special receiver parameter that comes before the function name. The receiver can be a value of type T or a pointer *T. Which one you pick changes what the method can do, how it behaves with copies, and how your type satisfies interfaces. This guide walks through every factor that matters when making that choice.
How a Receiver Behaves
Think of a method as a function where the receiver is the first argument. A value receiver gets a copy of the struct, exactly like passing a value to a function. A pointer receiver gets a pointer to the original value, so it can read and modify it.
type Counter struct {
value int
}
func (c Counter) Value() int {
return c.value
}
func (c *Counter) Increment() {
c.value++
}
Value receives a copy of the Counter. Changes to c inside Value would disappear when the method returns, so the method only reads data. Increment receives a pointer to the original Counter. When it writes c.value++, it changes the same struct that the caller holds.
It's just a function parameter:
Under the hood, func (c Counter) Value() becomes something like func Counter_Value(c Counter). There is no special magic — copying, aliasing, and address rules all work exactly as they would with any other function argument.
Two Reasons for a Pointer Receiver
The Go Tour and the official code review guidelines list two primary reasons to use a pointer receiver.
Mutation
If a method needs to change the receiver’s fields, the receiver must be a pointer. A value receiver only operates on a copy; the original is untouched.
package main
import "fmt"
type Account struct {
balance int
}
// BUG: value receiver cannot modify the original Account.
func (a Account) DepositCopy(amount int) {
a.balance += amount
}
// Correct: pointer receiver modifies the original.
func (a *Account) Deposit(amount int) {
a.balance += amount
}
func main() {
acc := Account{balance: 100}
acc.DepositCopy(50)
fmt.Println("After DepositCopy:", acc.balance) // 100
acc.Deposit(50)
fmt.Println("After Deposit:", acc.balance) // 150
}
DepositCopy silently fails to change the balance because it operates on a copy. A beginner might stare at the code and wonder why the balance never grows. The compiler will not warn you — the code is perfectly valid Go. The only reliable signal is the code review rule: if the method mutates state, the receiver must be a pointer.
Silent no-op mutations:
A value receiver that attempts to mutate the receiver is a common and dangerous mistake. The program compiles, runs without error, and produces wrong results. Always use a pointer receiver when the method needs to change the struct.
Avoiding Large Copies
Every value receiver call copies the entire struct. If the struct is large, the copy consumes time and memory. Pointer receivers copy only the pointer (8 bytes on 64-bit platforms), no matter how big the struct is.
type LargeReport struct {
Data [1024 * 1024]byte
}
func (r LargeReport) Summarize() int { // copies 1 MB every call
return len(r.Data)
}
func (r *LargeReport) Summarize() int { // copies only a pointer
return len(r.Data)
}
How large is large enough to care? A useful heuristic: imagine passing every field of the struct as a separate argument to the method. If that mental picture feels too heavy, use a pointer receiver.
When a Value Receiver Fits
A value receiver is the right choice when the type is small, naturally acts like a value, and needs no mutation. Think of types like time.Time — they are small, immutable from the caller’s perspective, and passing a copy is cheap and clear.
type Point struct {
X, Y float64
}
func (p Point) Distance() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
Here Distance does not change the point. Every call gets its own safe copy; no aliasing or shared state can surprise the caller. This matches how value types work in any language that distinguishes between objects and plain data.
Safe by default:
A value receiver guarantees that the method cannot alter the original struct — not accidentally and not through a bug. This is a strong correctness guarantee for small immutable data types.
Value receivers also avoid forcing heap allocation of the receiver. Small structs passed by value can often stay on the stack, reducing pressure on the garbage collector. But never choose a value receiver for this reason without profiling first; the compiler is smart enough to optimize many cases either way.
The Consistency Rule
The Go Tour states: “In general, all methods on a given type should have either value or pointer receivers, but not a mixture of both.” The reason becomes clear when you look at method sets.
A type’s method set is the set of methods you can call on it. For a type T, the method set contains all methods with value receivers. For *T, the method set contains all methods — both value and pointer receivers. If you mix receivers, the method sets of T and *T diverge in ways that break interface satisfaction.
type Data struct {
value int
}
func (d Data) Get() int { return d.value }
func (d *Data) Set(v int) { d.value = v }
type Setter interface {
Set(int)
}
func main() {
var s Setter
d := Data{}
s = d // compile error: Data does not implement Setter (Set method has pointer receiver)
s = &d // OK
}
Data has Get but does not have Set because Set requires a pointer receiver. Only *Data satisfies the Setter interface. If you write a function that accepts a Setter interface and someone passes a Data value, the code will not compile.
Interface mismatch from mixed receivers:
Mixing pointer and value receivers can make a type fail to satisfy an interface when used as a value, even though the methods exist somewhere. This leads to confusing compile errors in code that otherwise looks correct. Keeping receivers consistent avoids the problem entirely.
The official guidance is that if any method needs a pointer receiver, all methods should use pointer receivers, even read-only ones. This keeps the method set predictable.
Addressability and Automatic Dereference
Go helps you by automatically taking the address of a value or dereferencing a pointer when you call a method. If you call a value-receiver method on a pointer, Go dereferences the pointer for you. If you call a pointer-receiver method on an addressable value, Go takes its address.
d := Data{}
d.Set(10) // Go rewrites this to (&d).Set(10)
p := &Data{}
p.Get() // Go rewrites this to (*p).Get()
This convenience hides an important restriction: unaddressable values cannot have pointer methods called on them directly, because Go cannot obtain a stable address. Unaddressable values include function return values, map index results, and non-addressable expressions.
func makeData() Data { return Data{} }
makeData().Set(5) // compile error: cannot call pointer method on unaddressable value
There is no variable here, just a temporary value on the stack that will be discarded. Go refuses to give you a pointer to it because the pointer would be invalid after the expression. To call Set, you must first assign the result to a variable:
d := makeData()
d.Set(5) // works because d is addressable
This rule also applies to map values. Even though map index expressions are addressable in some languages, they are not in Go:
m := map[string]Data{"x": {}}
m["x"].Set(5) // compile error: cannot call pointer method on m["x"]
The workaround is to copy the value, modify it, and put it back:
d := m["x"]
d.Set(5)
m["x"] = d
Common Mistakes
Value receiver on a struct that contains a mutex
sync.Mutex and similar synchronization primitives must never be copied after first use. A value receiver copies the entire struct, including the mutex, giving each method call its own independent lock — which defeats the purpose.
Do not copy a Mutex:
If a struct contains a sync.Mutex, all its methods must use pointer receivers. Even read-only methods must use a pointer receiver to avoid copying the mutex. Running go vet will catch this mistake.
Using a pointer receiver for a tiny, immutable type out of habit
Not every struct needs a pointer. Small value types like a Point or a configuration struct that is never mutated are clearer with value receivers. They signal immutability and avoid forcing the caller to create a pointer.
Forgetting that a pointer method is not in the value method set
A beginner might write a function that accepts an interface expecting Set(int), then try to pass a Data value, and get a compile error. The fix is either to pass a pointer or to make the interface method use a value receiver (which usually means rethinking mutation).
Using the “consistency rule” blindly with value receivers that need mutation
If you decide “all my receivers will be value receivers” and then write a method that must mutate state, you will create a silent bug. Consistency is secondary to correctness — mutation always requires a pointer receiver.
Performance Is Not a Simple Rule
Benchmarks can show that a pointer receiver is faster for one struct and slower for another. A pointer forces the receiver to be addressable, which often means it escapes to the heap. The heap allocation and subsequent garbage collection can cost more than copying a small struct on the stack.
The compiler can sometimes optimize a pointer receiver call to avoid an escape when the receiver does not outlive the call, but this is not guaranteed. Value receivers give the compiler more freedom to keep data on the stack.
Profile before you decide:
Do not choose a receiver type solely for performance reasons unless you have a benchmark that proves the choice matters in your program. Correctness and clarity come first.
Summary
The receiver type is a design decision, not a syntax preference. Start with the question: does this method need to modify the struct it belongs to? If yes, it must be a pointer receiver. If no, ask whether the struct is large or contains a field that cannot be copied (like a mutex). If neither reason applies, a value receiver is perfectly fine — and often clearer — for small, immutable types.
When in doubt, the Go community defaults to a pointer receiver. It is the safer choice: it avoids accidental copies, allows mutation if needed later, and keeps the method set uniform. Just be ready to understand why that default exists and when it is worth overriding.