Methods
Methods are functions with a receiver — a special parameter before the function name that binds the function to a type.
Introduction
Methods are functions with a receiver — a special parameter before the function name that binds the function to a type. Receivers can be value or pointer; pointer receivers are required when methods mutate state or when the struct is large.
Go does not have classes — methods can be defined on any type in the same package, including non-struct types like custom int aliases. Method sets determine which methods a type satisfies for interfaces — pointer vs value receiver affects interface satisfaction.
This lesson covers receiver choice, method expressions, and the idiomatic patterns used in the standard library (http.Server methods, bytes.Buffer Write).
The story
A Cloudflare rate-limiter attaches behavior to a Limiter struct: func (l *Limiter) Allow(key string) bool. Pointer receivers mutate internal token buckets; value receivers suit immutable value types. Kubernetes' client-go defines methods on typed clients — the pattern you'll implement daily in backend services.
Choosing pointer vs value receiver consistently across a type prevents subtle bugs when interface satisfaction and mutation expectations diverge.
Understanding the topic
Key concepts
- func (r ReceiverType) MethodName(params) return — value receiver.
- func (r *ReceiverType) MethodName — pointer receiver for mutation.
- Method set: value type has value methods; pointer type has both value and pointer methods.
- Methods on non-struct types: type MyInt int; func (m MyInt) Double() int.
- No method overloading — one method name per receiver type.
- Interface satisfaction uses method set of the type used.
Step-by-step explanation
- Compiler desugars method call: v.Method(x) → Method(v, x).
- Value receiver copies struct — mutations don't persist.
- Pointer receiver shares instance — mutations visible.
- Go auto-dereferences: ptr.Method() works for pointer receiver.
- Method expression: f := Type.Method; f(instance, args).
- Consistency: if any method needs pointer receiver, use pointer for all.
Practical code example
Bank account with pointer receiver for mutation and value receiver for read:
package mainimport "fmt"type Account struct {owner stringbalance float64}func NewAccount(owner string) *Account {return &Account{owner: owner}}func (a *Account) Deposit(amount float64) {if amount > 0 {a.balance += amount}}func (a *Account) Withdraw(amount float64) error {if amount > a.balance {return fmt.Errorf("insufficient funds")}a.balance -= amountreturn nil}func (a Account) Owner() string {return a.owner}func main() {acc := NewAccount("Alice")acc.Deposit(500)_ = acc.Withdraw(100)fmt.Printf("%s balance: %.2f\n", acc.Owner(), acc.balance)}
Line-by-line code explanation
func (l *Limiter) Allow(key string) booldeclares a method with a pointer receiver.l.tokens--mutates receiver state — only possible with pointer receivers.func (p Point) Distance() float64uses a value receiver for small immutable types.(&l).Allow(key)Go auto-takes address when calling pointer methods on values.method setsmust be consistent — don't mix pointer and value receivers on the same type.type Handler interface { ServeHTTP(...) }interfaces are satisfied implicitly by implementing methods.func NewLimiter(r rate) *Limiterconstructor returns a pointer ready for method calls.method expressions: Limiter.Allowextracts a method as a function value.
Key takeaway: Use pointer receiver when mutating or struct is large. Be consistent within a type. Constructor returns *Account for method calls.
Real-world use
Where you'll use this in production
- Domain model behavior: order.Place(), account.Withdraw().
- HTTP handlers as methods on controller struct with dependencies.
- Custom types with validation: Email.Validate().
- Builder pattern: cfg.WithTimeout(d).WithRetries(n).
Best practices
- Use pointer receiver for mutation and large structs.
- Be consistent — all methods on a type use same receiver kind.
- Keep methods on domain types; free functions for stateless ops.
- Avoid getter/setter boilerplate — export fields when appropriate.
- Document exported methods with godoc.
Common mistakes
- Value receiver on mutating method — changes lost.
- Mixing value and pointer receivers — confusing interface satisfaction.
- Nil pointer receiver call — panics if method doesn't nil-check.
- Too many methods on one type — split responsibilities.
- Method on interface type — not allowed, only on concrete/named types.
Advanced interview questions
Q1BeginnerValue vs pointer receiver?
Q2BeginnerCan methods be on any type?
Q3IntermediateMethod set and interfaces?
Q4IntermediateWhen nil pointer receiver is OK?
Q5AdvancedDesign FileStore with Write, Read, Close methods.
Summary
Methods attach behavior to types via value or pointer receivers. Pointer receivers for mutation; be consistent across methods. Method set determines interface satisfaction. Constructors return pointers for method call convenience. Next lesson: interfaces — Go's polymorphism mechanism.