Skip to content
beginner Phase 4 · Structs, Methods, and Interfaces

Methods with Receivers

Attach functions to types and choose between value and pointer receivers.

40m
0 problems
Topic Progress 0%

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:

  1. 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.
  2. 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.
  3. The receiver is not a hidden pointer by default. func (a Account) really copies a on 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) ```

Question 1 options

2. `m := map[string]Counter{}` then `m["a"].Inc()` fails to compile where Inc has a pointer receiver. Why?

Question 2 options

3. A function returns `var pe *PathError; return pe` with signature `error`. Why does the caller's `if err != nil` branch execute?

Question 3 options

4. You need to pass a Counter's increment logic where a `func()` is expected. What is the most direct construction?

Question 4 options

Flashcards

Question

Where can you declare a method, and what does the receiver actually mean?

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?

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?

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?

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`?

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.

Related Reading