Functions With a Subject Line
Before methods, behavior about a value is expressed as a free function that takes the value as an argument:
func applyDiscount(acct Account, pct float64) Account {
acct.Balance -= acct.Balance * pct / 100
return acct
}
func remaining(acct Account) float64 { return acct.Balance }
Two costs show up immediately. Nothing groups these functions with Account — godoc lists them alphabetically among hundreds of package functions, and a caller has no way to discover what can be done to an account without reading the whole package. And every signature repeats the subject: applyDiscount(acct, ...), remaining(acct), reconcile(acct, ...). A method moves the subject into the declaration:
func (a Account) ApplyDiscount(pct float64) Account {
a.Balance -= a.Balance * pct / 100
return a
}
func (a Account) Remaining() float64 { return a.Balance }
acct := Account{Balance: 100}
acct = acct.ApplyDiscount(10)
fmt.Println(acct.Remaining()) // 90
The parenthesized name before the function — (a Account) — is the receiver. It binds one instance of the type to the method, and the call syntax acct.ApplyDiscount(10) reads as the type's own operation. Tooling follows automatically: go doc Account now lists its methods, and editors offer them on acct. completion.
Three structural rules govern where methods can live:
- The receiver's base type must be declared in the same package. You cannot add methods to
int,time.Time, or any imported type — the compiler says cannot define new methods on non-local type. To attach behavior to someone else's type, define your own type whose underlying type is theirs, or wrap it. - No overloading. One method name per receiver type, regardless of parameters — so a field and a method may not share a name on the same struct either.
- The receiver is not a hidden pointer by default.
func (a Account)really copiesaon every call. That copy is the subject of the next chapter, because choosing what gets copied — and whether mutations survive — is the one receiver decision you make every day.
Note what the receiver syntax did not introduce: there is no class, no this keyword, and no inheritance. A method is a function with an explicit first parameter; everything else follows from that.
Copy or Mutate: Choosing the Receiver
The receiver of func (a Account) is passed by value — a full copy of the struct. For a method that reports state, copying is correct. For a method that changes state, it is a bug:
type Counter struct{ n int }
func (c Counter) Inc() { c.n++ } // value receiver: mutates a copy
x := Counter{}
x.Inc()
fmt.Println(x.n) // 0 — the increment never reached x
Switch to a pointer receiver and the mutation lands:
func (c *Counter) Inc() { c.n++ }
Go's address rules fill in the rest of the picture. When you write x.Inc() with a pointer receiver, the compiler automatically takes &x. It can only do that when x is addressable — a variable, a pointer dereference, a slice element. Some values are not:
m := map[string]Counter{"a": {}}
m["a"].Inc() // compile error: cannot take the address of m["a"]
Map values live wherever the hash table decided, so there is no stable address to pass. The workaround is the pattern you will meet again in concurrency: store *Counter in the map, or copy the value out, mutate the copy, and write it back.
The decision procedure for real code:
| Receiver | Use when |
|---|---|
value (c Counter) |
method does not mutate, or returns a modified copy by design; type is small and all-value |
pointer (c *Counter) |
method mutates, or the struct is large enough that copying per call wastes work |
Two conventions keep the type coherent. Be consistent within a type — do not give Counter.Inc a pointer receiver and Counter.Reset a value receiver; callers stop being able to predict which calls persist, and interfaces get involved (next topic) the method set flips between Counter and *Counter. Mutation that must persist requires a pointer — a value receiver can still return a new value (a = a.ApplyDiscount(10) above), but it cannot modify the caller's variable in place.
Size matters less than it did decades ago — Go can copy large structs cheaply when escape analysis keeps them on the stack — but a 4 KB struct copied on every call in a hot loop is still a real cost. When unsure, prefer pointer receivers for anything that writes and let the linter tell you where copies hurt.
Methods That Survive a Nil Receiver
A method call is just a function call with the receiver as the first argument — and you are allowed to pass a nil pointer as that argument. The method runs; only field access would crash:
type PathError struct {
Path string
Msg string
}
func (e *PathError) Error() string {
if e == nil {
return "<nil>"
}
return "open " + e.Path + ": " + e.Msg
}
var pe *PathError
fmt.Println(pe.Error()) // <nil> — no panic, the method guarded itself
This nil-tolerance is a deliberate pattern for error types: callers can invoke .Error() on a value they suspect is absent, and the method answers rather than crashing. The guard if e == nil is what buys that — remove it and the first field access panics.
The pattern creates the most notorious nil bug in Go, worth writing out once:
func find(id int64) error {
var pe *PathError // nil *PathError
if id == 9 {
return pe // typed nil inside the interface
}
return nil
}
if err := find(9); err != nil {
fmt.Println("failure:", err) // runs! prints failure: <nil>
}
find returns a typed nil: the interface value holds the pair (*PathError, nil), so err != nil is true even though the pointer inside is nil. An interface is nil only when both its type and value are absent. The fix is discipline at the boundary: return a literal nil when there is no error, never a nil pointer of a concrete type. This bug is one reason the interface topic comes next — it is not a quirk of methods, it is what an interface value actually contains (Interfaces and Implicit Satisfaction).
A smaller trap, same family: a nil-receiver method works on a nil pointer, so var p *PathError; p.Error() is safe while the zero struct PathError{} is also safe — but a nil interface has no method to call at all:
var e error
// e.Error() panics: nil interface, no dynamic type, no method dispatch
The rule to carry: methods run on nil pointers of their own type (guard them), and never call a method on an interface you have not checked for nil.
Passing a Method Like a Function
Because a method is a function with a bound first parameter, you can detach it. Two forms exist, and the difference is which argument supplies the receiver.
A method value binds the receiver now and gives you an ordinary function:
c := &Counter{}
callbacks := map[string]func(){
"increment": c.Inc, // receiver bound: c
}
callbacks["increment"]() // same as c.Inc()
The value c.Inc remembers c at the moment you wrote it — pass it to anything expecting func(), store it in a struct field, hand it to time.AfterFunc, and it will act on that receiver whenever called. This is the bridge between methods and the closure world (Anonymous Functions and Closures): the bound method is a function value like any other.
A method expression keeps the receiver as an explicit parameter:
fn := Counter.Inc // type func(*Counter)
fn(c) // c is now the argument
Counter.Inc(c) // written out in full
Useful when the receiver arrives at runtime — a table of operations keyed by type, generic dispatch code, or tests that want to call methods without an instance. It also makes the "methods are just functions" fact explicit: Counter.Inc has exactly the same type as func(*Counter).
Where each earns its place:
| Form | Type of the result | Typical use |
|---|---|---|
c.Inc |
func() with c already bound |
callbacks, handler tables, deferred calls |
Counter.Inc |
func(*Counter) — receiver is arg 1 |
generic plumbing, runtime receiver, test helpers |
One caution with bound methods: the binding is a reference, not a snapshot. If c is a pointer, later mutations of c are visible through the stored func(); the bound value pins the receiver variable, exactly the capture rule closures follow. And if the receiver was a value — x.Inc where Inc has a value receiver — the method value bound a copy, so mutations go nowhere.
Methods, bound methods, and plain functions are one continuum in Go: the receiver is just the first argument with better ergonomics. That continuity is what makes the next topic — swapping concrete types behind one interface — feel like a small step instead of a new language.
Quiz
1. What does this print? ```go type C struct{ n int } func (c C) Inc() { c.n++ } func (c *C) Add() { c.n += 10 } x := C{} x.Inc() x.Add() fmt.Println(x.n) ```
2. `m := map[string]Counter{}` then `m["a"].Inc()` fails to compile where Inc has a pointer receiver. Why?
3. A function returns `var pe *PathError; return pe` with signature `error`. Why does the caller's `if err != nil` branch execute?
4. You need to pass a Counter's increment logic where a `func()` is expected. What is the most direct construction?
Flashcards
Question
Where can you declare a method, and what does the receiver actually mean?
Click to reveal answer
Answer
Only on types declared in your own package — you cannot add methods to int or time.Time. The receiver is simply an explicit first parameter: `func (c *Counter) Inc()` is a function whose first argument is a *Counter, which is why method values and method expressions exist.
Question
What is the concrete rule for choosing a value receiver versus a pointer receiver?
Click to reveal answer
Answer
Pointer when the method mutates state or the struct is large; value when the method only reads or deliberately returns a modified copy. Keep one style per type — mixing flips the method set between Counter and *Counter and confuses both callers and interfaces.
Question
Why does `m["k"].MutatingMethod()` fail when the method has a pointer receiver?
Click to reveal answer
Answer
Map elements are not addressable, so the compiler cannot produce the *T the method requires. Variables, fields, pointer dereferences, and slice elements are addressable; map values and function results are not — copy out, mutate, write back.
Question
Can a method run when the receiver is a nil pointer? What must you do?
Click to reveal answer
Answer
Yes — the method executes and only crashes if it dereferences fields, so guard with `if e == nil` at the top. This is how error types implement Error() safely for nil instances; a nil interface, by contrast, has no method to call at all.
Question
Difference between the method value `c.Inc` and the method expression `Counter.Inc`?
Click to reveal answer
Answer
`c.Inc` is func() with the receiver already bound (mutations follow the receiver if it is a pointer). `Counter.Inc` is func(*Counter) — the receiver is the first argument, supplied at each call, which is what you want when the receiver arrives at runtime.
Revision Notes
Key Takeaways
- 1. A method is a function with a receiver parameter — no classes, no this, no inheritance involved
- 2. Value receivers copy; pointer receivers mutate — a value-receiver increment on a local copy changes nothing upstream
- 3. Pointer-receiver calls require an addressable value, which is why map elements reject them
- 4. Methods execute with nil pointer receivers if guarded, but a typed nil returned as error defeats err != nil
- 5. Method values (c.Inc) and expressions (Counter.Inc) detach methods into plain functions for callbacks and dispatch
Interview Tips
- • "Value or pointer receiver?" — mutation or size says pointer; pure reads say value; add the consistency rule and the interface method-set consequence
- • "Why can't I call this method on a map value?" — not addressable; show the copy-out/mutate/write-back workaround
- • "Explain the typed nil bug" — interface holds (type, value); a nil *PathError is a non-nil error; fix is returning literal nil at the boundary
- • "How do you pass a method as a callback?" — method value `c.Inc` binds the receiver and yields func(); mention the bound variable is live, not a snapshot
Cheat Sheet
Methods with Receivers
Declaration: func (r T) Name(args) results — receiver is an explicit first parameter; base type must be declared in the same package; no overloading; a field and method may not share a name.
Receiver decision:
| Situation | Receiver | Why |
|---|---|---|
| mutates state | pointer | mutation must reach the caller's value |
| reads only, small struct | value | cheap copy, no aliasing surprises |
| large struct, hot path | pointer | avoids per-call copies |
| method returns modified copy by design | value | caller reassigns: x = x.Method() |
Addressability: pointer-receiver calls need an address — map elements and function results do not have one; slice elements do.
Nil rules: nil pointer receiver runs (guard if r == nil); a typed nil returned as an interface is NOT an interface nil — return literal nil for "no error."
Code template:
type Counter struct {
n int
}
func (c *Counter) Inc() { c.n++ }
func (c Counter) Value() int { return c.n }
func example() {
c := &Counter{}
var inc func() = c.Inc // method value: receiver bound
inc()
}
Continue Learning
Before You Start
- No prerequisites listed.