Method receivers in Go: the copy you cannot see, and the "pointer is faster" that is wrong
An interview on receivers climbs a ladder: what a receiver is — why the change did not stick — why the type "does not implement" the interface — when the compiler takes the address for you and when it refuses — and what the copy costs. The lesson climbs all of it: the measurement shows no difference at all up to eight words, and a tenfold one at a kilobyte.
Full technical treatment
TL;DR
A method with a value receiver works on a copy; a method with a pointer
receiver can change the original value. The reason is simple: a receiver is
the method's first argument, merely written on the left, and arguments in Go
are passed by copy. The call looks the same either way: c.Inc() compiles in
both cases.
Hence the three places where that difference surfaces. The method sets of
T and *T differ: a method with a pointer receiver is not in the value's
method set — hence Point does not implement Stringer, checked through
reflect: Point gives false, *Point gives true. The compiler takes
the address for you, but only of something addressable: c.IncPointer()
works on a variable and does not compile at all on a map element. And
for _, v := range gives a copy of the element, so a pointer method changes
that copy rather than the slice — with the compiler saying nothing.
"A pointer receiver is faster" is not a rule but a function of size. Measured on one and the same method: up to 8 words there is no difference (×1.01), at 32 words ×2.71, at 128 — ×9.5. The numbers come from one run on one machine, so what carries meaning is how the series moves, not any single ratio.
- types can have methods, and a method is called as
c.Inc(); - an argument passed to a function is a copy: changing it inside does not change the original;
- Go has pointers:
&cis the address of a value, and through it the value can be changed.
- what a method set is, what addressability is, when the compiler takes an address for you;
reflect,copylocks, escaping to the heap, the price of a copy in nanoseconds.
What is really being asked
The ladder is almost always this one:
- "How does a value receiver differ from a pointer receiver?" — testing whether you say "a copy" or stop at the word "mutates".
- "Why did my method change nothing?" (with code) — that same copy.
- "Why does the type not implement the interface?" — about method sets.
- "Does Go take the address itself? Always?" — about addressability; here most people answer "always", and that is wrong.
- "What do you pick by default and why?" — testing whether you name consistency of the method set rather than "a pointer, it is faster".
- "How much faster?" — a question about honesty: without a number both extremes are equally bad.
The lesson climbs that ladder, and it climbs in one direction: semantics first — does the method change state, and what follows from that for method sets and interfaces — and only then cost. Its spine is one sentence: a receiver is an ordinary argument, and arguments are copied.
Base: the one question the choice starts from
A method in Go is a function tied to a type: c.Inc() calls Inc "on" the
value c. That value is called the receiver, and it is declared to the left
of the method's name: func (c Counter) Inc().
The whole topic rests on one distinction, and it is a semantic one. A method with a value receiver works on a copy: whatever it changes there is invisible outside. A method with a pointer receiver gets an address and can change the original value.
So the choice of receiver is decided not by the size of the struct and not by team habit, but by one question about what the method means:
Does the method work with a copy of the value — or must it change the original state?
If it changes it, the receiver must be a pointer; there is no alternative. If it does not, the other considerations follow below: method sets, the decision tree and, last of all, the price of the copy.
Why the question is that one becomes clear if you read the declaration as an ordinary function:
func (c Counter) IncValue() { c.n++ }
func (c *Counter) IncPointer() { c.n++ }Those are IncValue(c Counter) and IncPointer(c *Counter). The difference is
exactly the one between any two functions with those signatures — there is no
magic in a receiver.
Two conclusions follow at once:
- A value receiver gets a copy. Incrementing a field of the copy is allowed and pointless: the copy dies when the method returns.
- A pointer receiver gets an address. One word is copied, and it leads to the original.
The trap is that the call looks identical: c.IncValue() and
c.IncPointer() are written exactly the same way. The difference is visible
only in the method declaration — elsewhere in the file, often in another file.
The rule about pointers vs. values for receivers is that value methods can be
invoked on pointers and values, but pointer methods can only be invoked on
pointers.
The second half of that sentence is the source of both bugs in this lesson: "the method changed nothing" and "the type does not implement the interface".
That is already enough to answer the basic interview question: the difference between receivers is the difference between working on a copy and working on the original. Everything below is about what follows from it for interfaces, where the compiler supplies an address for you, where a copy appears unnoticed, and what it costs.
Mechanism 1: a method set is not a matter of style
The method sets of T and *T are different, and the difference is visible
without a compiler — through reflect:
| methods | |
|---|---|
Point | Len |
*Point | Len, String |
String is declared with a pointer receiver and did not make it into the
value's method set. The interface check says the same:
implements Stringer | |
|---|---|
Point | false |
*Point | true |
The reason is not syntactic, and it is worth being able to state: a value in an interface is a copy, and it has no address. A method with a pointer receiver could not change the original; silently changing a copy would be worse than a build error. The other direction is allowed: a pointer has an address, and dereferencing it for a value method is always possible.
The specification puts it in one sentence:
The method set of a defined type T consists of all methods declared with
receiver type T. The method set of a pointer to a defined type T … is the set of
all methods declared with receiver *T or T.
Mechanism 2: the compiler takes the address — but not everywhere
Here is where nearly everyone answers "Go will take the address itself", which is only half right.
First, what it is not. Taking the address automatically is syntactic
convenience, not a change to the method set. The set of T stays what it was:
the compiler merely writes & where it is allowed to. Which is precisely why
assigning a value to an interface, from the previous section, is not rescued by
the same mechanism — there is no longer a variable whose address could be taken.
The condition is spelled out in the specification:
if x is addressable and &x's method set contains m, x.m() is shorthand for
(&x).m()
The key word is addressable. What is addressable and what is not is worth holding as a list:
| expression | addressable | c.IncPointer() |
|---|---|---|
a variable c | yes | works |
a field of a variable s.field | yes | works |
a slice element xs[i] | yes | works |
a map element m["k"] | no | does not compile |
a function result f() | no | does not compile |
a constant or literal Counter{} | no | does not compile |
A variable has an address, so c.IncPointer() works. A map element has none — the table is rebuilt on growth — and
m := map[string]Counter{"k": {}}
m["k"].IncPointer() // does not compilewill not build at all: cannot call pointer method on m["k"]. The way around it
is map[string]*Counter: the pointer moves, the object stays put.
And that is, oddly, the good news. Where there is no address the compiler refuses to build and the error is immediate. The dangerous case is where an address does exist: there the compiler quietly takes the address of a copy — and that is the next section.
Mechanism 3: three places where a copy appears unnoticed
All three are common "find the bug" questions, and none is caught by the
compiler. The numbers below were printed by a run of
bench/gorecv/internals.go.
First — range over a slice of values. The loop variable is a copy of the
element:
for _, v := range items {
v.Add() // changes the copy; the slice stays put
}
for i := range items {
items[i].Add() // changes the element
}The run gives 0 in the first case and 1 in the second. This is the most
common of the three, because for _, v := range is written without thinking.
Second — assigning a struct. cp := src copies it whole, and from then on
they are two independent objects: after cp.Add() the run shows src=5 cp=6.
Third — keeping values where pointers are needed. map[string]Acc will not
let a pointer method be called at all; []Acc will, but through an index rather
than through the loop variable.
What all three share: the call looks the same. The compiler takes the address when the variable is addressable, and says nothing; when there is no address, it refuses to build. The bug lives between those two cases.
Mechanism 4: the decision tree
The rule for choosing is not about speed. The questions are asked in order, and the first "yes" settles it:
The top three questions are about correctness, and only the fourth about cost. The same questions in words:
- Does the method mutate the receiver? Then a pointer — otherwise it mutates a copy. That is not a recommendation but the only working option.
- Does the type contain something that must not be copied?
sync.Mutex,sync.WaitGroup,strings.Builder— a copy of such a type breaks silently, andgo vetcatches it with a dedicatedcopylockscheck. - Do some methods already have pointer receivers? Then give the rest pointer receivers too: otherwise the method set depends on what exactly was assigned.
if some of the methods of the type must have pointer receivers, the rest should
too, so the method set is consistent regardless of how the type is used
Only if none of the three questions settled it does size remain. Good candidates
for a value receiver are small immutable types: time.Time, your own wrappers
over a number or a string. There a copy is a feature rather than a cost.
Deeper: "a pointer is faster" is a function of size, not a rule
Here are the numbers worth switching the tab above for. The same method — reading two fields — over structs of different sizes:
| size | bytes | value | pointer | ratio |
|---|---|---|---|---|
| 1 word | 8 | 2.25 ns | 2.24 ns | 1.01 |
| 2 words | 16 | 1.92 | 1.94 | 0.99 |
| 8 words | 64 | 2.10 | 1.93 | 1.09 |
| 32 words | 256 | 5.24 | 1.93 | 2.71 |
| 128 words | 1024 | 19.88 | 2.09 | 9.50 |
This table should be read bottom-up. At the bottom is what people ask about: a copy costs money, and at a kilobyte the call is nearly ten times dearer. At the top is what people forget: at one or two words there is no difference at all — there is nothing to copy, the value lives in registers anyway.
So the answer "a pointer receiver is faster" is wrong as a rule. But the opposite rule — "take the value, a copy is cheap" — is wrong in exactly the same way: the bottom row of the table refutes it.
What is right is a third thing, and it is not a rule but a dependency: the copy is what costs, and its price grows with the size. The method body is the same throughout — the same two fields are read. The ratios themselves belong to this run on this machine: elsewhere they will come out different, while the direction of the series will not.
And size is only one of the factors, and the least important at that. A pointer makes the value escape to the heap more often (see the escape-analysis lesson), while a value gives the compiler more freedom. Which wins in a given function is measured with a profile, not derived from a table.
if the receiver is large, a big struct for instance, it will be much cheaper to
use a pointer receiver
Note the word "large" — and note that in the FAQ this caveat comes after the rules about mutation and consistency, not instead of them.
How to answer in an interview
Short answer: a method with a value receiver works on a copy, while a method with a pointer receiver can change the original value. A receiver is the method's first argument written on the left, and arguments are copied; hence both "the method changed nothing" and "the type does not implement the interface".
That is enough for a correct answer. What follows is what you add when the interviewer digs.
If the interviewer digs deeper
To "what is the difference" answer "a receiver is the first argument". "A value is copied, a pointer is not; everything else follows." The answer shows straight away that you did not memorise two rules but understand one.
On "the method changed nothing" name the place, not the rule. "Most likely
for _, v := range: the loop variable is a copy, and the pointer method changed
it." This reads as experience immediately.
On the interface, talk about the address. "A copy in an interface has no address, so a method with a pointer receiver is not in the value's method set." "You have to pass a pointer" is true but explains nothing.
On automatic address-taking, add the caveat. "It does, if the expression is addressable; a map element has no address, and there it does not compile." Half of candidates answer "always".
On speed, give a range rather than an extreme. "The difference is the price of the copy: up to eight words there is none at all, at a kilobyte I got ten times." One number without the other turns a right measurement into a wrong rule.
On the default, name consistency. "If even one method needs a pointer, all of them get pointers, so the method set does not depend on what was assigned."
Next they ask
What if half a type's methods take a value and half take a pointer?
It compiles, and that is the trouble. The method set starts depending on what
exactly you assigned: T implements one interface, *T another. The error
turns up not where it was made but in someone else's package, where a value was
put into an interface.
Hence the FAQ's recommendation: consistency beats a local optimisation. There is one exception — methods that must work on the type's zero value without changing it — and even there it is usually simpler to make everything pointers.
Can a method be declared on a slice or a map?
On your own named type, yes: type Stack []int and then
func (s *Stack) Push(v int). On []int directly, no: the receiver must be a
type defined in the same package.
A subtlety about Stack: the pointer receiver is needed precisely for Push,
because append may return a new header. A value method would get a copy of the
header, put the element into it and lose the result — the same copy problem,
just three words wide.
What does go vet say about copying a mutex?
The copylocks check catches a copy of a type containing a sync.Locker:
assignment, passing to a function by value and — most importantly — a value
receiver on a method of such a type. The message reads passes lock by value.
The reason is that a copy of a mutex does not inherit its state: a copy of a
locked mutex arrives unlocked, and the protection vanishes without a single
error. go vet runs as part of go test, so this check is probably already
working in your CI.
Can a method be called on a nil pointer?
Yes, and it is legal: a method with a pointer receiver gets nil as an ordinary
value. A crash happens only on dereferencing a field, and a method that touches
no fields runs fine.
Idioms are even built on this: a tree method func (t *Node) Size() int with an
early if t == nil { return 0 } frees the caller from checks. It also makes
clear why *Point implements Stringer even when the pointer is nil: a method
set is a property of the type, not of the value.
How does the receiver relate to the typed-nil trap?
Directly. If Error() is declared with a pointer receiver, then *MyErr
implements error, not MyErr — so a pointer is what goes into the interface.
And a nil pointer in an interface produces that famous err != nil with no
error present.
So two topics — method sets and the two words of an interface — meet in one bug.
The receiver decides what ends up in the interface, and the interface's
layout decides how that compares against nil.
Is a pointer receiver worth it to "optimise" a small struct?
The measurement says no: up to eight words there is no difference at all. And the decision has a price — a pointer can force the value onto the heap, and then you have traded a free copy for an allocation and work for the collector.
So the order is: the mandatory part first (mutation, non-copyable fields, consistency), size second. "A pointer just in case" is optimisation by guess, and it is sometimes negative.
Common misconceptions
a receiver is a special language construct
It is the first argument, written to the left of the method name. func (c Counter) Inc() is the same thing as func Inc(c Counter). Everything follows at once: arguments are copied, so a value receiver gets a copy.
a pointer method cannot be called on a value
It can — if the value is addressable: the compiler substitutes (&x).m(). It fails only where there is no address: a map element, a function result, a value in an interface. The word "cannot" hides exactly the condition being asked about.
Go always takes the address for you
Only for an addressable expression. m["k"].IncPointer() on a map[string]Counter does not compile: the map is rebuilt on growth and an element has no address. The cure is map[string]*Counter.
a pointer receiver is faster
That is a function of size, not a rule. Measured on the very same method: 1 word — ×1.01, 2 words — ×0.99, 8 words — ×1.09, 32 words — ×2.71, 128 words — ×9.50. Up to eight words there is no difference, because there is nothing to copy.
in for _, v := range you can modify v
You can, but it is a copy of the element and the slice will not change: the run gives 0 instead of 1. The compiler says nothing because v is addressable and a pointer method applies to it. Modify through the index: items[i].Add().
if the type has the method, it implements the interface
Only if the method is in the method set of what is being assigned. reflect shows it plainly: Point implements Stringer — false, *Point — true, with one method declared.
mixing receivers on one type is fine
It compiles, but the method set starts depending on what was assigned: T implements one interface, *T another. The language FAQ recommends consistency outright, and the reason is practical: the error turns up somewhere other than where it was made.
a method on a nil pointer always panics
Not always: a pointer receiver takes nil as an ordinary value, and the panic comes only from dereferencing a field. Idioms are built on this, such as func (t *Node) Size() int with an early if t == nil.
Practice
Two problems. Answer first, then check against the real output: in both, the correct answer is taken from a run of the script rather than assigned.
Practice · predict the output
func (c Counter) IncValue() { c.n++ }
func (c *Counter) IncPointer() { c.n++ }
c := Counter{}
c.IncValue()
c.IncPointer()
fmt.Println(c.n)
cs := []Counter{{}, {}}
for _, v := range cs {
v.IncPointer()
}
fmt.Println(cs[0].n)
for i := range cs {
cs[i].IncPointer()
}
fmt.Println(cs[0].n)
p := &Counter{}
p.IncValue()
fmt.Println(p.n)Practice · estimate
Knowledge check
items is a []Acc. What does for _, v := range items { v.Add() } do if Add is declared with a pointer receiver?
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- A method with a value receiver works on a copy; a method with a pointer receiver can change the original value. The reason is simple: a receiver is the method's first argument, merely written on the left, and arguments in Go are passed by copy. The call looks the same either way:
c.Inc()compiles in both cases. - Hence the three places where that difference surfaces. The method sets of
Tand*Tdiffer: a method with a pointer receiver is not in the value's method set — hencePoint does not implement Stringer, checked throughreflect:Pointgives false,*Pointgives true. The compiler takes the address for you, but only of something addressable:c.IncPointer()works on a variable and does not compile at all on a map element. Andfor _, v := rangegives a copy of the element, so a pointer method changes that copy rather than the slice — with the compiler saying nothing. - "A pointer receiver is faster" is not a rule but a function of size. Measured on one and the same method: up to 8 words there is no difference (×1.01), at 32 words ×2.71, at 128 — ×9.5. The numbers come from one run on one machine, so what carries meaning is how the series moves, not any single ratio.
In fact
- It is the first argument, written to the left of the method name.
func (c Counter) Inc()is the same thing asfunc Inc(c Counter). Everything follows at once: arguments are copied, so a value receiver gets a copy. - It can — if the value is addressable: the compiler substitutes
(&x).m(). It fails only where there is no address: a map element, a function result, a value in an interface. The word "cannot" hides exactly the condition being asked about. - Only for an addressable expression.
m["k"].IncPointer()on amap[string]Counterdoes not compile: the map is rebuilt on growth and an element has no address. The cure ismap[string]*Counter. - That is a function of size, not a rule. Measured on the very same method: 1 word — ×1.01, 2 words — ×0.99, 8 words — ×1.09, 32 words — ×2.71, 128 words — ×9.50. Up to eight words there is no difference, because there is nothing to copy.
- You can, but it is a copy of the element and the slice will not change: the run gives
0instead of1. The compiler says nothing becausevis addressable and a pointer method applies to it. Modify through the index:items[i].Add(). - Only if the method is in the method set of what is being assigned.
reflectshows it plainly:PointimplementsStringer— false,*Point— true, with one method declared. - It compiles, but the method set starts depending on what was assigned:
Timplements one interface,*Tanother. The language FAQ recommends consistency outright, and the reason is practical: the error turns up somewhere other than where it was made. - Not always: a pointer receiver takes
nilas an ordinary value, and the panic comes only from dereferencing a field. Idioms are built on this, such asfunc (t *Node) Size() intwith an earlyif t == nil.
What is covered
- What is really being asked
- Base: the one question the choice starts from
- Mechanism 1: a method set is not a matter of style
- Mechanism 2: the compiler takes the address — but not everywhere
- Mechanism 3: three places where a copy appears unnoticed
- Mechanism 4: the decision tree
- Deeper: "a pointer is faster" is a function of size, not a rule
- How to answer in an interview
- Next they ask
- Common misconceptions
- Practice
- Knowledge check
Sources & further reading
3 SOURCES
- The Go specification — Method sets, Method declarations, Calls, SelectorsOfficial documentation. The rule the whole topic is derived from: «The method set of a defined type T consists of all methods declared with receiver type T. The method set of a pointer to a defined type T … is the set of all methods declared with receiver *T or T». And on when the compiler takes the address itself: «A method call x.m() is valid if the method set of (the type of) x contains m … if x is addressable and &x's method set contains m, x.m() is shorthand for (&x).m()».https://go.dev/ref/spec#Method_sets
- Go FAQ — Should I define methods on values or pointers?Official documentation. The language authors' own answer to this interview question. On mutation: «if the method needs to mutate the receiver, the receiver must be a pointer». On consistency: «if some of the methods of the type must have pointer receivers, the rest should too, so the method set is consistent regardless of how the type is used». And the efficiency caveat: «if the receiver is large, a big struct for instance, it will be much cheaper to use a pointer receiver».https://go.dev/doc/faq#methods_on_values_or_pointers
- Effective Go — Pointers vs. ValuesOfficial documentation. On addressability being part of the language rather than an implementation detail: «The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers».https://go.dev/doc/effective_go#pointers_vs_values