Last Updated: May 17, 2026
Every method in Go attaches to a receiver, and that receiver is either a value or a pointer. The choice changes whether the method sees a copy or the real thing, whether it can mutate the underlying value, and how the type fits into larger programs. This lesson covers the syntax, the semantics, the decision rules, and the pitfalls that come up when the wrong receiver gets picked.
A method declaration starts with a receiver in parentheses right after the func keyword. The receiver has a name and a type. The type is either a struct type directly (a value receiver) or a pointer to that type (a pointer receiver).
The two methods look almost identical, but the asterisk in *Product changes everything that happens at runtime. Display gets a fresh copy of the product on every call. Restock gets a pointer to the caller's product, so the assignment to p.Stock writes through that pointer back into the original.
The receiver name is just a variable. By convention it's short (often the first letter of the type, lowercase), but it follows the same scoping rules as a function parameter. You read and write its fields with the dot operator regardless of whether the receiver is a value or a pointer; Go automatically dereferences the pointer when you write p.Stock. There's no p->Stock syntax to learn.
The call site looks the same too: mug.Display() and mug.Restock(5) both use plain dot syntax. Go inserts the address-of (&mug) for the pointer receiver call when needed, so you don't write (&mug).Restock(5) by hand. The rules around when this auto-conversion works (and when it doesn't) belong to method sets.
A value receiver gets a complete copy of the receiver every time the method is called. Reads inside the method see the caller's data at the moment of the call. Writes inside the method change the copy and the copy alone, and the changes vanish when the method returns.
TryDiscount writes to p.Price, but p is a local copy. The original book in main is untouched. This is the same value-copy behavior covered with structs, applied to a method instead of a regular function. A method with a value receiver behaves exactly like a regular function that takes the struct as its first argument.
If the intent was actually to mutate the product's price, the value receiver is the wrong tool. Compile the code, run it, and the price won't change. There's no warning. The compiler does what was asked: it copies, mutates the copy, and throws the copy away. This is the most common mistake people hit early on with receivers, and we'll come back to it.
Cost: every call to a value-receiver method copies the entire struct. For a small product struct with a few fields, the copy is a handful of bytes and effectively free. For a struct with embedded large arrays, big nested structs, or many fields, the copy is real work on every call.
A pointer receiver gets a pointer to the caller's value. Reads inside the method see the live data. Writes go through the pointer and change the original. There's no copy of the struct involved, just a copy of the pointer itself, which is one machine word.
Now the change sticks. ApplyDiscount declared its receiver as *Product, and p inside the method is a pointer to book in main. Assigning to p.Price modifies the field of the original struct, not a copy. After the method returns, book.Price is the new discounted price.
The call site is identical to the value-receiver version: book.ApplyDiscount(10). Go saw that ApplyDiscount wanted a *Product, noticed that book is addressable (it's a local variable in main), and quietly took its address for the call. The shortcut works as long as the value on the left of the dot has a memory address Go can take. Calling a pointer-receiver method on something temporary (like the result of a function call) is where this breaks down, and it's a method-set topic we'll come back to.
The diagram contrasts the two call shapes. On the value side, the method's p is a fresh struct sitting in its own memory, populated by copying the caller's fields. Any change to p.Price happens in that copy, and the caller never sees it. On the pointer side, the method's p is a small pointer value, but it refers back to the caller's struct. Writing through p updates the original in place.
The single most important rule about receivers is this: only pointer receivers can mutate the underlying value in a way the caller will see. Any method that needs to change a field, append to a slice field, or reset state has to use a pointer receiver.
Consider a cart with an AddItem method. The natural intent is to grow the cart's items slice. With a value receiver, the slice header gets copied, the append might allocate a new backing array, and the caller's cart sees nothing.
The cart stays empty. AddItem got a copy of Cart, and append produced a slice that lived only inside that copy. When the method returned, the new slice header went away with it. The caller's cart.Items is still its original nil slice.
The fix is a pointer receiver:
Now c is a pointer to the caller's cart. The assignment c.Items = append(...) updates the slice header of the caller's struct, not a throwaway copy. Both items make it in.
There's one place where a value receiver appears to mutate something, and it trips people up. When a struct contains a slice or a map field, the slice header is copied, but both copies share the same backing array (for slices) or hash table (for maps). Writing to an existing index works through both. Growing the slice with append doesn't.
The caller's cart.Items is changed even though Rename has a value receiver. This isn't a contradiction. The slice header was copied into the method's c, but the header's pointer field still points at the same backing array as the caller's slice. Writing to c.Items[0] writes into that shared array, which the caller's slice also sees. If Rename had instead done c.Items = append(c.Items, ...), the caller wouldn't see anything, because the assignment to the local slice header doesn't reach back.
The takeaway: a value receiver can sometimes affect data the caller can see, but only by accident when a shared backing array is involved. As a rule, if a method's purpose is to mutate, declare it with a pointer receiver. That makes the intent obvious, the behavior predictable, and the slice-header trap a non-issue.
The call syntax is the same for both receiver kinds: value.Method(args). Go figures out the rest. When the method has a pointer receiver and you call it on an addressable value, the compiler inserts the address-of for you.
Both calls work without any explicit &. The first one is the auto-conversion in action: book.Restock(3) becomes (&book).Restock(3) at the compiler's choosing. The second is the direct case: bookPtr is already a *Product, so the call needs no adjustment.
The same flexibility runs the other way. A method with a value receiver can be called on a pointer; Go inserts the dereference:
Display wants a Product, not a *Product, but the call still compiles. Go quietly dereferences bookPtr to get the value, copies it into the receiver, and runs the method. The shorthand applies symmetrically.
The catch is that the auto-address-of only works when the value on the left is addressable: a local variable, a struct field of an addressable struct, an element of an addressable slice. Composite literals, function return values, and map elements are not addressable, and calling a pointer-receiver method on them fails to compile.
There are real rules to picking between value and pointer receivers, and most of them aren't about taste. They follow from how Go's type system and runtime work. The Go team's own style guide and the Effective Go document spell out the same set of considerations. Here's the short version, in order of importance.
Rule 1: Use a pointer receiver if the method needs to mutate the receiver. This is the hard requirement, not a preference. A method whose purpose is to change a field, grow a slice, or update state has to use a pointer receiver, or it won't work. Cart's AddItem, Product's Restock, Account's Deposit: all pointer receivers.
Rule 2: Use a pointer receiver if the struct contains a `sync.Mutex` or any field that must not be copied. Copying a sync.Mutex (or sync.RWMutex, sync.WaitGroup, atomic.Int64, and friends) breaks them. A value receiver copies the whole struct, which copies the mutex, which is a bug. The go vet tool will flag this, and the safe default for any type with a mutex field is pointer receivers everywhere.
Rule 3: Use a pointer receiver if the struct is large. Copying a large struct on every method call is wasted work. There's no fixed cutoff, but a useful guideline is: if the struct is bigger than a couple of machine words, prefer pointer receivers. Linus's "one cache line" heuristic is a reasonable rule of thumb too. For tiny structs (one or two small fields), a value receiver is fine and avoids one level of indirection.
Rule 4: Use a value receiver when the method just reads and the type is small and conceptually a value. A Point{X, Y}, a Money{Amount, Currency}, an RGB{R, G, B}: these are small, immutable-in-spirit, and a value receiver fits perfectly. Copying them is cheap, and a value receiver communicates "I won't change anything."
Rule 5: Be consistent. This is the rule that ties everything together: don't mix value and pointer receivers on the same type. Pick one style for the type and stick with it across all its methods.
A small comparison summarizes the trade-offs:
| Receiver | Copies struct? | Can mutate? | Works with sync.Mutex fields? | Typical use |
|---|---|---|---|---|
Value (p Product) | Yes, every call | No, mutations are lost | No (vet flags copies) | Small, read-only types |
Pointer (p *Product) | No, copies pointer only | Yes | Yes | Mutation, large structs, types with non-copyable fields |
The rules layer. If the method must mutate, use a pointer (rule 1). If the type contains a mutex, use a pointer (rule 2). If neither applies but the struct is large, use a pointer (rule 3). Otherwise, a value receiver is fine, and you'd pick it for small read-only types (rule 4). And whichever you pick, apply it consistently (rule 5).
The consistency rule deserves its own section because it has a real consequence beyond style. When the methods on a type mix value and pointer receivers, the type's method set splits in a way that affects interface satisfaction.
Here's the problem in plain code form. Suppose a Product type has some value-receiver methods and some pointer-receiver methods:
This compiles and runs. As a single-file example, mixing receivers seems harmless. The issues show up later:
Product value satisfies any interface whose methods all match its value-receiver methods. It does not satisfy an interface that requires a pointer-receiver method, unless you pass &book instead of book.*Product value satisfies both value-receiver and pointer-receiver methods.The Go community's standing advice is: pick one style per type. If any method on the type needs a pointer receiver (because of mutation, mutex, or size), use pointer receivers for every method on the type, even the read-only ones. The cost is a tiny extra indirection on the read methods. The win is a single, predictable method set and a clear story when the type starts implementing interfaces.
When in doubt, default to pointer receivers across the type. The Go authors picked this convention for most of the standard library, and you'll see it in popular libraries too.
Three pitfalls trip people up more than any others. They're worth knowing on sight because the compiler doesn't always catch them, and the bugs are silent.
**Pitfall 1: Forgetting the * and silently working on a copy.**
The intent is clearly to count. The result is zero. The cause is the missing *: func (c Counter) Inc() declares a value receiver, so each call increments a fresh copy. Switching to func (c *Counter) Inc() fixes it. This is the most common Go-receiver bug, and it costs roughly one rerun to spot. After being burned once, most developers default to pointer receivers for anything that looks like mutation.
Pitfall 2: Pointer-receiver method on a non-addressable value.
The compiler reports something like cannot call pointer method Discount on Product or cannot take the address of makeProduct(). The return value of makeProduct() is a temporary value with no address, so Go can't auto-insert & to make the call. The fix is to assign the return value to a variable first:
Same issue applies to map elements: m["key"].Mutate() fails for the same reason.
Pitfall 3: Copying a struct with a `sync.Mutex` field.
Value receivers copy everything, including any embedded mutexes, channels, or wait groups. Those types are designed to be referenced, not copied, and copying them silently breaks the synchronization. The go vet tool flags this (assignment copies lock value), and the standard library types like sync.Mutex and sync.WaitGroup have explicit "do not copy after first use" warnings in their docs.
go vet will complain about this code. Even if it didn't, two goroutines calling Add would each lock their own copy of the mutex, providing zero protection. The fix is a pointer receiver, and the rule generalizes: any type with a mutex field uses pointer receivers throughout.
A small example pulls the rules together. A Cart has items, a discount code, and a few operations: add an item, total it up, and display it. Some methods mutate, some don't. The consistency rule says all of them should use pointer receivers.
AddItem, ApplyDiscount, and the more obvious mutators need pointer receivers. Total and Display don't mutate, but they're declared with pointer receivers too for consistency. Reviewers don't have to wonder which methods change the cart and which don't. They all use the same receiver style, the method set is unified, and the type behaves predictably when it later starts implementing interfaces.
If Total were declared with a value receiver, the code would still compile and produce the same output here. It would just be inconsistent with the rest of the type, and that inconsistency would matter once interfaces or method-value expressions enter the picture.
sync.Mutex or other non-copyable field, or when the struct is large enough that the per-call copy matters.go vet tool catches the most common receiver bugs: copying a struct with a sync.Mutex, and a few related lock-copy patterns.The next lesson, Method Sets and Addressability, takes the auto-conversion shortcut apart. It covers exactly which methods belong to a type's method set, why value types and pointer types have different sets, and the addressability rules that decide whether something.PointerMethod() compiles.