Pointer Receivers vs Value Receivers
A method’s receiver is the special parameter that appears before the method name and binds the method to a type — and whether you write it as a value (p Point) or a pointer (p *Point) changes everything about how the method behaves. A value receiver operates on a private copy of the struct, so changes made inside the method vanish the moment it returns. A pointer receiver operates on the original data through its memory address, so changes stick. Picking the right one is one of the most consequential early decisions in designing a Go type, and it also determines whether a type satisfies an interface.
Overview: How Method Receivers Work
In Go, a method is nothing more than an ordinary function that has one extra, implicit parameter — the receiver. The compiler treats a method like MoveBy on Point almost exactly like a free function func MoveBy(p Point, dx, dy int), except it is called with dot syntax: p.MoveBy(5, 5) instead of MoveBy(p, 5, 5). That mental model — "a method is a function whose first argument is spelled before the dot" — explains almost every quirk of receivers.
Because Go passes arguments by value, a value receiver means Go copies the entire struct into the receiver parameter before the method body runs. Any field you modify inside the method is a modification to that copy; the original variable back in the caller is untouched, and the copy is discarded when the method returns. A pointer receiver, in contrast, receives the memory address of the original value. There is no copy of the struct (only a copy of the pointer, which is cheap — the size of a machine word), and any field you modify through that pointer is a modification to the original data, visible to the caller after the method returns.
This has two practical consequences beyond "does it mutate." First, cost: copying a large struct on every method call is real work; a pointer receiver avoids that copy no matter how big the struct is, since a pointer is always the same small size. Second, correctness with interfaces: which methods a type has depends on the receiver, and that determines which interfaces a type — or a pointer to that type — satisfies. We come back to this in detail below, because it is the single most common receiver-related compile error beginners hit.
One nuance worth internalizing early: slices, maps, and channels are themselves small header values that point at shared underlying data. If a struct has a slice field and you use a value receiver, Go copies the slice header (pointer, length, capacity) — not the underlying array. A method with a value receiver can therefore still mutate the elements a slice points to, even though it cannot change what the caller sees about the struct’s own fields. This is a common source of confusion, and it is why the safe rule is: if a method needs to change what the caller observes in any way, use a pointer receiver, full stop.
Syntax
The general form of a method declaration is:
func (receiver ReceiverType) MethodName(params ...Type) ReturnType {
// value receiver — receiver is a copy
}
func (receiver *ReceiverType) MethodName(params ...Type) ReturnType {
// pointer receiver — receiver is the original value's address
}
- receiver — a short variable name (by Go convention one or two letters, not
thisorself) used inside the method body to refer to the receiver. - ReceiverType — the named type the method is attached to. It must be declared in the same package as the method; you cannot add methods to types from other packages, including built-ins like
intorstring. - * — present only on pointer receivers; makes the receiver a pointer to
ReceiverTypeinstead of a copy of it. - params / ReturnType — identical to an ordinary function signature.
Examples
Example 1: A Value Receiver That Doesn’t Mutate
package main
import "fmt"
type Point struct {
X, Y int
}
func (p Point) MoveBy(dx, dy int) {
p.X += dx
p.Y += dy
}
func main() {
p := Point{X: 1, Y: 2}
p.MoveBy(5, 5)
fmt.Println(p)
}
Output:
{1 2}
Because MoveBy has a value receiver, calling it copies p into a new Point inside the method. The copy’s fields are updated, but that copy is thrown away when the method returns, so the p back in main is completely unchanged.
Example 2: A Pointer Receiver That Mutates
package main
import "fmt"
type Point struct {
X, Y int
}
func (p *Point) MoveBy(dx, dy int) {
p.X += dx
p.Y += dy
}
func main() {
p := Point{X: 1, Y: 2}
p.MoveBy(5, 5)
fmt.Println(p)
}
Output:
{6 7}
Only the receiver’s type changed from Point to *Point. Now MoveBy receives the address of p, so p.X += dx modifies the original struct through that address. Notice the call site, p.MoveBy(5, 5), did not change at all — Go automatically took the address of p for us. That automatic behavior is explained in the next section.
Example 3: A Realistic Bank Account Type
package main
import (
"errors"
"fmt"
)
type Account struct {
Owner string
Balance float64
}
func (a *Account) Deposit(amount float64) {
a.Balance += amount
}
func (a *Account) Withdraw(amount float64) error {
if amount > a.Balance {
return errors.New("insufficient funds")
}
a.Balance -= amount
return nil
}
func main() {
acc := &Account{Owner: "Maria", Balance: 100}
acc.Deposit(50)
if err := acc.Withdraw(200); err != nil {
fmt.Println("withdraw failed:", err)
}
fmt.Println(acc.Owner, acc.Balance)
}
Output:
withdraw failed: insufficient funds
Maria 150
This is the idiomatic shape of a mutable Go type: both methods use pointer receivers because both can change Balance, and errors are returned as ordinary values instead of thrown as exceptions. Deposit raises the balance from 100 to 150; Withdraw asks for 200, which exceeds the balance, so it returns an error instead of mutating anything, and the balance stays at 150.
How It Works Step by Step
When you call a pointer-receiver method on a plain value, Go automatically takes the address of that value for you — but only if the value is addressable. A local variable, a struct field, or a slice element are all addressable, so p.MoveBy(5, 5) is silently rewritten by the compiler to (&p).MoveBy(5, 5). The reverse also works: calling a value-receiver method through a pointer is rewritten to dereference it first, so pp.Speak() for a *Dog becomes (*pp).Speak().
Not every value is addressable. Map values, results returned directly from a function call, and composite literals used inline as an expression are not addressable, so the compiler cannot silently take their address. That is exactly why calling a pointer-receiver method directly on a map element fails to compile — see Common Mistakes below.
This automatic rewriting is also the basis for method sets, the rule Go uses to decide which methods count as belonging to a type when checking interface satisfaction:
| Methods declared with | In method set of T |
In method set of *T |
|---|---|---|
| value receiver | yes | yes |
| pointer receiver | no | yes |
In other words, the pointer type *T can do everything T can, plus more. This is why assigning a value of type T (instead of &T) to an interface variable fails to compile whenever any method the interface needs has a pointer receiver — the value’s method set is simply missing it.
package main
import "fmt"
type Point struct {
X, Y int
}
func (p *Point) MoveBy(dx, dy int) {
p.X += dx
p.Y += dy
}
func main() {
p := Point{X: 1, Y: 2}
p.MoveBy(5, 5)
pp := &Point{X: 10, Y: 10}
pp.MoveBy(5, 5)
fmt.Println(p, *pp)
}
Output:
{6 7} {15 15}
Here p is a plain Point variable, so p.MoveBy(5, 5) is rewritten to (&p).MoveBy(5, 5) and mutates it to {6 7}. pp is already a *Point, so its call needs no rewriting. Printing *pp dereferences the pointer to show the underlying struct, {15 15}.
Common Mistakes
Mistake 1: Expecting a Value Receiver to Mutate the Struct
package main
import "fmt"
type Counter struct {
count int
}
func (c Counter) Increment() {
c.count++
}
func main() {
c := Counter{}
c.Increment()
c.Increment()
fmt.Println(c.count)
}
Output:
0
This compiles cleanly — there is no error, which is exactly what makes it dangerous. Increment has a value receiver, so every call operates on a fresh copy of Counter that is discarded when the method returns; count in main never moves. The fix is a pointer receiver:
package main
import "fmt"
type Counter struct {
count int
}
func (c *Counter) Increment() {
c.count++
}
func main() {
c := Counter{}
c.Increment()
c.Increment()
fmt.Println(c.count)
}
Output:
2
Mistake 2: Inconsistent Receivers Breaking Interface Satisfaction
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d *Dog) Speak() string {
return d.Name + " says Woof"
}
func main() {
var s Speaker = Dog{Name: "Rex"}
fmt.Println(s.Speak())
}
This fails to compile with an error along the lines of "Dog does not implement Speaker (Speak method has pointer receiver)." Speak is declared on *Dog, so it is only in the method set of *Dog, not Dog. Assigning a plain Dog{} value to a Speaker variable requires Dog itself to have that method, which it doesn’t. The fix is to store a pointer:
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d *Dog) Speak() string {
return d.Name + " says Woof"
}
func main() {
var s Speaker = &Dog{Name: "Rex"}
fmt.Println(s.Speak())
}
Output:
Rex says Woof
Mistake 3: Calling a Pointer-Receiver Method on an Unaddressable Map Value
package main
type Point struct {
X, Y int
}
func (p *Point) MoveBy(dx, dy int) {
p.X += dx
p.Y += dy
}
func main() {
m := map[string]Point{"a": {X: 1, Y: 2}}
m["a"].MoveBy(1, 1)
}
This fails to compile with "cannot call pointer method on m["a"]" because map elements are not addressable — the map could rehash or move its internal storage at any time, so Go refuses to let you take the address of a value stored inside one. The usual fix is to store pointers in the map instead of values:
package main
import "fmt"
type Point struct {
X, Y int
}
func (p *Point) MoveBy(dx, dy int) {
p.X += dx
p.Y += dy
}
func main() {
m := map[string]*Point{"a": {X: 1, Y: 2}}
m["a"].MoveBy(1, 1)
fmt.Println(*m["a"])
}
Output:
{2 3}
Now m["a"] is itself a *Point value (readable straight out of the map, no addressing needed), so calling a pointer-receiver method on it works directly.
Best Practices
- Use a pointer receiver whenever the method needs to modify the receiver’s fields — a value receiver’s mutation is silently lost.
- Use a pointer receiver for large structs, even for read-only methods, to avoid the cost of copying on every call.
- Keep every method on a type consistent: once one method needs a pointer receiver, make all of that type’s methods pointer receivers, so callers never have to remember which is which.
- Prefer value receivers for small, simple types that behave like values (a coordinate pair, a money amount, a duration-style wrapper) where copying is cheap and immutability is a feature.
- Remember that a pointer-receiver method is only in the method set of the pointer type — pass or store
&T, notT, when the receiving code needs to satisfy an interface requiring those methods. - When you need to mutate a struct stored in a map, store
*Tin the map instead ofT, since map elements are not addressable. - Calling a pointer-receiver method through a nil pointer is legal as long as the method body never dereferences a nil field — useful deliberately (a nil-safe
String()method), but guard against it when it isn’t intentional. - Run
go vetregularly; it flags some receiver-related mistakes, such as copying a type that embeds async.Mutex.
Practice Exercises
- Define a
Rectanglestruct withWidthandHeight float64fields and a pointer-receiver methodScale(factor float64)that multiplies both fields byfactor. Call it onRectangle{Width: 4, Height: 2}withfactor = 2.5and print the result. Expected output:{10 5}. - Take the broken
Dog/Speakerexample from Common Mistakes and fix it two different ways: once by changing how the value is assigned to the interface, and once by changingSpeakto a value receiver instead. Add a short comment explaining why each fix works. - Write a
Stacktype wrapping a[]intwith pointer-receiver methodsPush(v int)andPop() (int, bool), where the boolean reports whether the stack was non-empty. Push 1, 2, 3, then pop twice and print each result. Expected output:3 truethen2 true.
Summary
- A method is a function with an implicit receiver parameter;
p.M()is sugar for callingMwithp(or&p) as that parameter. - Value receivers get a copy of the struct — mutations inside the method do not affect the caller’s original.
- Pointer receivers get the original’s address — mutations are visible to the caller, and no struct copy is made.
- Go automatically takes the address of an addressable value to call a pointer-receiver method, but cannot do this for unaddressable values like map elements.
- The method set of
*Tincludes both pointer- and value-receiver methods; the method set ofTincludes only value-receiver methods — this decides interface satisfaction. - Once a type has any pointer-receiver method, make all of its methods pointer receivers for consistency.
