Last Updated: May 22, 2026
A pointer receiver is the bridge between Go's pointer rules and methods. This chapter focuses on the mechanics: what the receiver actually holds at runtime, the syntax for declaring it, the addressability rules that decide whether something.Method() even compiles, and what happens when you call a pointer-receiver method on a nil pointer. The question of when to pick a pointer receiver is a separate design discussion; this chapter answers "how does this thing actually work?".
A method's receiver is a parameter. The compiler treats func (p *Product) ApplyDiscount(pct float64) almost exactly the way it treats func ApplyDiscount(p *Product, pct float64). The receiver gets bound to the value on the left of the dot at the call site, and from there it behaves like any other function argument.
Inside the method, p is a local variable of type *Product. Its value is the address of book in main. When the method writes p.Price = ..., it's storing into the memory p points at, which is book's memory. The method ends, p goes out of scope, but the write to book.Price is already done.
The receiver block sits between func and the method name, the same place a value receiver lives. The asterisk in *Product is the only thing that changes the runtime behavior. The compiler reads the asterisk and knows to pass an address instead of a copy.
The two declarations look almost identical, but the second one is asking for a pointer. That single character has implications for what the method can see, what it can change, and which call sites are legal. The rest of this chapter unpacks each piece.
The diagram traces a single call. The caller's book lives in main's stack frame. The call binds p to the address of book. Inside the method body, writing through p reaches back into book's memory. When the method returns, the modified Price is visible to the caller.
The full shape of a pointer receiver method is:
Three things to note:
*TypeName, with an asterisk before the type. The asterisk is part of the type, not the name.p for Product, c for Cart, o for Order)._ if the method doesn't use the receiver inside its body. Most methods do use it, so _ is rare.Here's a Product with two pointer-receiver methods:
Both methods declare p *Product as their receiver. Both mutate fields through p. The call sites look identical to value-receiver calls; the compiler handles the pointer-passing automatically.
There's no p->Field arrow syntax in Go like there is in C or C++. You read and write fields through a pointer receiver with the regular dot operator, and Go handles the implicit dereference. The next section covers exactly why.
p.Price, Not (*p).PriceWhen p is a *Product, the expression p.Price is automatic shorthand for (*p).Price. Go inserts the dereference for you whenever you access a field or call a method through a pointer. This is the same rule for selector expressions, applied inside a method body.
The body applies the discount twice on purpose, once with the short form and once with the explicit dereference. Both are valid, both compile, both update the same field through the same pointer. Idiomatic Go uses the short form everywhere. The explicit (*p).Price form is needed only in the rare cases where it disambiguates an expression, and even then it's unusual.
The short form also works for method calls. If Product has another pointer-receiver method Display, you can call it from inside ApplyDiscount as p.Display(). Go dereferences p, looks up Display in Product's method set, and binds the address back to the new method's receiver. The chain works because every step preserves the pointer.
The first call binds p to &book. The discount update happens through p. The follow-up call p.Display() reuses the same address, so Display sees the freshly discounted price. There's no copy at any step.
The implicit dereference covers field access and method calls. It doesn't cover the case where you want the entire underlying value, not just one of its fields. For that, write *p.
The clearest example is overwriting the whole struct at once. Suppose a method needs to reset the receiver back to a zero state, or replace it with a freshly constructed value:
The line *c = Cart{} says: at the address c points to, store a freshly zeroed Cart. Every field gets its zero value. The caller's cart is wiped without ever having to assign each field individually.
Two things separate this from field-by-field writes:
*c = Cart{} is one assignment of a whole struct. The compiler emits a single store of the struct's worth of memory.c = &Cart{} doesn't do what you might think, which a later section covers.You'll also see explicit dereferences when a method wants to pass the receiver's value (not its address) to something that expects the value type. For example, printing a copy or passing it to a function that takes Cart by value:
Snapshot is a pointer-receiver method, but it wants to hand a Cart value to snapshot, which takes Cart by value. The explicit *c dereferences the pointer and produces a Cart value, which the call site then copies into snapshot's parameter. Note that both copies still share the same backing array for the Items slice, so this isn't a deep copy, but the slice header is separate. Appending to cart.Items reassigns the live cart's slice header but doesn't affect snap's header.
The general rule: use plain p when you mean "the pointer", p.Field when you mean "the field at that pointer", and *p when you mean "the whole value at that pointer".
A pointer-receiver method needs an address to bind its receiver to. When you call something.PointerMethod(), Go either has a *Type already (in which case the call is direct) or it has a Type value and needs to take its address (in which case the value has to be *addressable*).
A value is addressable if Go can give you its memory address. Some things are addressable, some aren't, and the rules are baked into the language spec.
| Expression | Addressable? | Pointer-receiver call works? |
|---|---|---|
Local variable (book) | Yes | Yes |
Field of an addressable struct (cart.Items) | Yes | Yes |
Element of an array variable (products[0]) | Yes | Yes |
Element of a slice (products[0]) | Yes | Yes |
Pointer dereference (*ptr) | Yes | Yes |
Map element (stock["BOOK-01"]) | No | No (compile error) |
Function return value (getProduct()) | No | No (compile error) |
Composite literal (Product{...}) | No | No directly (but &Product{...} works) |
| Constant or untyped value | No | No |
The "compile error" rows are the ones that surprise newcomers. Let's walk through each.
A local variable of struct type sits in memory the compiler can address. Calling a pointer-receiver method on it works without any explicit &:
book is a local variable. The compiler knows where it lives in the current stack frame, so it can take its address. The call book.ApplyDiscount(10) becomes, in effect, (&book).ApplyDiscount(10). The address-of is implicit.
*T Values: Direct, No Conversion NeededIf you already have a *Product, the call is direct. There's nothing for the compiler to convert because the receiver type and the value type already match.
book here is a *Product. The method wants *Product. Direct match, no work.
Slice elements live in a backing array that Go can address. You can take &products[0] directly, and you can call pointer-receiver methods on a slice element without any extra syntax:
Each products[i] is addressable, so products[0].ApplyDiscount(10) is shorthand for (&products[0]).ApplyDiscount(10). The method updates the slice element in place. After the loop, both products carry their discounted prices.
The for _, p := range products loop, though, copies each element into p. If you tried to call p.ApplyDiscount(...) inside that loop, you'd be mutating the loop variable's copy, not the underlying slice element. To mutate slice elements in a loop, use an index:
That keeps the call rooted at an addressable slice element.
Cost: Iterating for _, p := range products copies each Product into p. For tiny structs this is cheap. For large ones, prefer for i := range products and use products[i] directly.
Map elements are the most famous addressability gotcha in Go. A map's internal storage can be rearranged on insertion or growth, so the address of any particular value isn't stable. Go solves this by making map elements unaddressable. You can read them, you can replace them, you can't take their address, and you can't call a pointer-receiver method on one.
The compiler rejects this with:
The standard workaround is one of two patterns:
Option 1 stores *Product values, so each lookup yields a pointer that already has the right type. The pointer-receiver call works directly because there's no auto-address-of needed. Option 2 reads a copy into a local variable (which is addressable), mutates that, and writes it back. Both are common patterns.
A value returned from a function exists only as a temporary in the call expression. There's no stable variable backing it, so it has no address.
Compiler error:
The fix is to assign the return value to a local variable, which gives it an address:
Or return a pointer in the first place. A function that returns *Product lets the caller call pointer-receiver methods directly:
The same rule covers composite literals at a call site. Product{Price: 24.99}.ApplyDiscount(10) fails the same way, because the literal is a temporary. You'd write (&Product{Price: 24.99}).ApplyDiscount(10) instead, which makes the literal's address explicit.
A pointer can be nil. A pointer-receiver method is just a function that takes a pointer as its first argument. The call itself works when the pointer is nil. What blows up is the moment the method tries to read or write a field through that nil pointer.
The first call binds p to nil. Inside the method, the nil check catches it and returns early. No field access, no crash. The second call binds p to a real address, and the field reads work normally.
Compare that to a method that dereferences without the check:
The call compiles fine. The runtime crashes the moment p.Price tries to load the price from address zero.
The takeaway: nil receivers are a valid runtime state in Go, and a pointer-receiver method can choose to handle them gracefully. The standard library does this in a few well-known places. A nil *bytes.Buffer panics on most operations, but a nil *http.Request.URL and several other types have methods that work on nil receivers (or at least don't crash). When you're writing your own types, decide explicitly: if a method can be called with a nil receiver, check for nil up front. If it can't, document that and let the panic happen.
The behavior is sometimes useful for constructing read-only "empty" cases without allocating an instance. A linked-list Length method can return zero for a nil receiver, a tree's Find can return "not found", and so on:
The recursion bottoms out when n.Next is nil. The next call binds n to nil, the method checks, returns zero, and the chain collapses. The same idea would be impossible with a value receiver: a WishlistNode value can't represent "no node".
Two patterns that look similar do completely different things inside a pointer-receiver method:
*p = Cart{} writes a new value at the address p points to. The caller sees the change.p = &Cart{} reassigns the local p to point somewhere else. The caller sees nothing.The reason is that p itself is a local copy of the pointer. The address it holds was passed in by the caller, but the variable p lives in the method's stack frame. Reassigning p only changes that local copy. The caller's pointer (if it was a pointer) or the caller's value (if Go auto-took the address) doesn't move.
ResetByRebind makes a fresh &Cart{} and stores it in c. The caller's cart is untouched. As soon as the method returns, c is discarded along with the freshly allocated cart it briefly pointed at.
ResetByDeref doesn't move c. It takes the address c already holds and overwrites the whole struct at that address with Cart{}. The caller's cart is at that exact address, so the caller sees the reset.
The diagram contrasts the two cases. Rebinding the local pointer is wasted work; the caller is on a different track. Writing through the pointer reaches the caller's memory and the change persists.
The same principle covers a subtler case. If your method wants to replace one pointer with another (say, swap a *Cart field on a wrapper for a fresh cart), you have to write through the receiver, not rebind it:
NewCartBroken reassigns s, which is a local copy of the pointer. The caller's session is unaffected. NewCart writes through s by assigning to s.Cart, which is the same as (*s).Cart = &Cart{}. The caller's session field now points at the fresh cart.
The rule to remember: the receiver is a parameter. Treat it like any other parameter. Changes to p itself stay local. Changes through p (writing to a field or to *p) reach the caller.