Lesson 6. Methods and Method Sets
Mission link: The receiver choice looks like a mutation question and is really a type question. It decides, silently, whether your type satisfies the interface you are about to write.
Primary source: Go FAQ: Should I define methods on values or pointers?
Prerequisites: Lesson 2, Lesson 4
Warm-up
- ▢
for i, r := range "héllo": why doesijump from 1 to 3?
Check
i is a byte index and é occupies two bytes. range decodes UTF-8 and yields runes, so it skips past the continuation byte.
- ▢ A function takes a
[]int, appends to it, and does not return it. What does the caller see?
Check
Nothing new. The new length only ever lands in the local copy of the header. Whether the element was written into shared storage depends on capacity, but the caller's length never changes.
Know this
A method is a function with a receiver, declared on any named type in the same package, not only structs:
type Celsius float64
func (c Celsius) String() string { return fmt.Sprintf("%.1f°C", float64(c)) }
Receivers are named with one or two letters, used consistently across every method on the type. c, not this, not self, and not a different name in each method.
The two receiver forms
func (c Counter) Value() int { return c.n } // value receiver, operates on a copy
func (c *Counter) Inc() { c.n++ } // pointer receiver, operates on the original
Go inserts the address or the dereference for you when the value is addressable:
var c Counter
c.Inc() // shorthand for (&c).Inc(), c is a variable, so it has an address
Addressability is where it stops being automatic. Map elements and function results have no address, so a pointer method cannot be called on them:
m := map[string]Counter{"a": {}}
m["a"].Inc() // compile error: cannot call pointer method Inc on Counter
Same rule as the unassignable map field from Lesson 4, wearing a different error message.
The method set is what interfaces see
Here is the part that surprises people, and the reason this is its own lesson:
| Receiver of the method | In the method set of T | In the method set of *T |
|---|---|---|
value, func (t T) | yes | yes |
pointer, func (t *T) | no | yes |
Method sets are asymmetric. *T has everything; T has only the value-receiver methods. Calling through a variable hides this, since c.Inc() works fine, but interface satisfaction does not:
type Incrementer interface{ Inc() }
var _ Incrementer = &Counter{} // ok
var _ Incrementer = Counter{} // compile error: Inc method has pointer receiver
Satisfaction is membership, not a rule to memorise: Inc() is a row in one box and an empty slot in the other, and the check that fails is the one looking in the box without it.
The reason is that an interface holds a copy of the value put into it, and that copy has no address. Letting Counter{} satisfy Incrementer would mean calling Inc on a copy nobody can name, mutating something no one can observe. Rather than allow that, the language removes the method from the set.
var _ Incrementer = (*Counter)(nil) at package level is the idiomatic compile-time assertion that the relationship holds. It costs nothing at runtime and it fails the build the day someone changes a receiver.
Pick one form per type
The rule that keeps this manageable: choose value or pointer receivers per type, not per method. Mixing them means the method set depends on which method you are asking about, and callers have to track which form they are holding.
Use pointer receivers when any method mutates, when the type contains a sync.Mutex or another copy-hostile field, or when the type is already handled through pointers everywhere. Use value receivers for small immutable types: time.Time and the Celsius above are the shape. When in doubt on a struct, pointer receivers are the safer default, because adding a mutating method later then costs nothing.
Practice
- ▢
Counterhas onlyfunc (c *Counter) Inc(). Which compiles:var c Counter; c.Inc(), orvar i Incrementer = Counter{}?
Check
The first compiles, the second does not.
c is a variable and therefore addressable, so Go rewrites the call as (&c).Inc(). The interface assignment has no variable to address, because it copies the Counter into the interface, so Inc is not in Counter's method set and the assignment is rejected.
The lesson is that a working method call tells you nothing about interface satisfaction.
- ▢ Why does
m["a"].Inc()fail whenmismap[string]Counter, but succeed whenmismap[string]*Counter?
Check
In the first case the element is not addressable, so Go cannot produce the *Counter that Inc requires. In the second the map already stores a pointer; nothing needs addressing, and the method is called on the value the map handed back.
This is the same constraint as m["a"].N++ in Lesson 4: the map refusing to hand out addresses into storage it may relocate.
-
▢ A type has
func (s Server) Name() stringandfunc (s *Server) Start() error. What doesvar _ Runner = Server{}do, whereRunnerrequires both methods?- a) Compiles, and
Startoperates on a copy - b) Compiles, but panics when
Startruns - c) Fails to compile, because
Startneeds a pointer - d) Fails to compile, because receivers are mixed
- a) Compiles, and
Check
c) Fails to compile, because Start needs a pointer.
Server's method set contains only Name. Mixing receiver forms is a style problem rather than a compile error, so d names a real smell but not the reason this line is rejected. &Server{} satisfies Runner.
- ▢ You add a
sync.Mutexfield to a struct whose methods all use value receivers. Name what breaks.
Check
Every method call copies the struct, and therefore the mutex. Each copy locks its own mutex, so nothing is actually excluded and the type provides no protection while appearing to.
go vet's copylocks check reports the copy. The fix is to convert the type to pointer receivers throughout, which is the practical argument for choosing them by default on structs that might ever grow state.
- ▢ Interleaving Lesson 1: is
var c Counter, with no constructor, ready forc.Inc()?
Check
Yes, provided every field is usable at its zero value, which n int and an embedded sync.Mutex both are.
The zero-value rule and the receiver rule meet here: the type is usable unconstructed, and it must be used through a pointer. NewCounter() *Counter earns its place only if it has something real to set.
Real-world reps
- [ ] Write
Counterwith a pointer-receiverIncand a value-receiverValue. Then declarevar _ Incrementer = Counter{}and read the compile error carefully. It names the receiver, which is the clue you will want later. - [ ] Take a type you have written in another language with getters and setters. Write the Go version and count how many methods survive. Most getters do not.
- [ ] Tomorrow: find a struct in a real codebase with mixed receivers. Work out whether the mix was deliberate. Note what it would take to unify them.
Going further
- Go FAQ: methods on values or pointers
- Method sets, in the language spec
- Go Code Review Comments: receiver type: the reasoning a reviewer will cite
- Lesson 11. Interfaces Are Satisfied Implicitly: where method sets start to bite
- Resources
Not landing? Reread the primary source at the top, since this lesson compresses it and compression is where understanding leaks. Check the glossary for any term that felt slippery.
If the lesson itself is unclear rather than the material, that is a defect: open an issue.