Methods: Value vs Pointer Receivers
Methods attach behavior to types through receivers - either a copy (T) or a pointer (*T).
The choice affects mutability, allocations, interface satisfaction, and API consistency across your package.
Summary
A value receiver copies the struct for the method call.
Mutations inside the method do not affect the caller's copy.
A pointer receiver shares the underlying value.
Mutations persist and avoid copying large structs.
Method sets determine interface satisfaction: value type T only includes methods with value receivers; pointer type *T includes both value and pointer receiver methods.
Pick one receiver style per type unless you have a documented exception.
Use pointer receivers when methods mutate state or when struct size makes copying costly.
Recipe
Quick-reference recipe card - copy-paste ready.
type Buffer struct {
b []byte
}
// Pointer receiver: mutates and matches io.Writer-style APIs.
func (buf *Buffer) Write(p []byte) (int, error) {
buf.b = append(buf.b, p...)
return len(p), nil
}
// Value receiver: read-only snapshot.
func (buf Buffer) Bytes() []byte {
return append([]byte(nil), buf.b...)
}
// Consistency: if any method needs *T, prefer *T for all methods.
func (buf *Buffer) Reset() { buf.b = buf.b[:0] }When to reach for this:
- Struct methods mutate fields (
Inc,Write,SetState). - Types implement interfaces requiring pointer methods (
fmt.Stringeron large structs is fine either way;sync.Mutexmust not copy). - Value types are small immutable identifiers (
time.Timeuses value methods). - You need stable interface satisfaction for both
Tand*Tassignments.
Working Example
package ledger
import "fmt"
type Account struct {
id string
balance int64
}
func NewAccount(id string) *Account {
return &Account{id: id}
}
func (a *Account) Credit(cents int64) error {
if cents < 0 {
return fmt.Errorf("ledger: negative credit %d", cents)
}
a.balance += cents
return nil
}
func (a Account) Balance() int64 {
return a.balance
}
type Creditor interface {
Credit(cents int64) error
}
func Process(c Creditor, amount int64) error {
return c.Credit(amount)
}
func Example() error {
acct := NewAccount("user-1")
if err := Process(acct, 500); err != nil {
return err
}
return nil
}What this demonstrates:
- Pointer receiver on
Creditmutates balance. - Value receiver on
Balancereturns a snapshot without exposing mutable state. - Interface
CreditorrequiresCredit; satisfied by*Account, notAccountvalue. - Constructor returns
*Accountso callers land on the pointer method set.
Deep Dive
How It Works
- Methods desugar to functions:
a.Credit(x)becomesCredit(a, x)with receiver as first argument. - Compiler passes address automatically for pointer receivers when variable is addressable.
- Method sets:
- For type
T: methods with receiverT - For type
*T: methods with receiverTand*T
- For type
- Interface assignment checks method set of the dynamic type held.
Receiver Selection Guide
| Signal | Prefer |
|---|---|
| Mutates receiver fields | *T |
Struct contains sync.Mutex or similar no-copy fields | *T only |
| Small immutable value type | T |
| Large struct (> few pointers worth) | *T for performance |
| Mixed receivers on same type | Avoid - pick *T if any mutation exists |
Interface Satisfaction Table
| Method receiver | var x T satisfies? | var p *T satisfies? |
|---|---|---|
func (T) M() | Yes | Yes |
func (*T) M() | No | Yes |
Go Notes
type S struct{ n int }
func (s S) V() int { return s.n }
func (s *S) P() int { s.n++; return s.n }
var i interface {
V() int
P() int
}
// i = S{} // compile error: S missing P
i = &S{} // OKDocument constructors returning *T when pointer methods exist.
Gotchas
- Value receiver on mutating method - Changes disappear after call. Fix: switch to
*Tor return updated copy explicitly. - Copying structs with mutexes - Value receivers duplicate
sync.Mutexand panic. Fix: pointer receivers only; sometimes unexport struct. - Inconsistent receivers -
func (T) Fooandfunc (*T) BarbreaksTinterface assignments. Fix: unify on*Twhen any pointer method exists. - Nil pointer receiver calls - Go allows
var p *T; p.Method()if method handles nil (seenilguards in stdlib). Fix: document or panic early for invalid nil. - Interface holds value with pointer methods - Assigning
Accountvalue toCreditorfails. Fix: store*Accountor add value-receiver wrappers only when semantically sound. - Embedding pointer types - Promoted methods may require addressable outer struct. Fix: understand promoted method sets when embedding
*bytes.Buffer. - JSON / protobuf generated types - Often use pointers for optional fields; do not mix custom value receivers that copy generated state unexpectedly.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Package functions func F(t *T) | No method sets needed | Implementing standard interfaces |
Immutable return-new pattern func (t T) WithX() T | Small value types like config | Large structs or hot paths |
| Interface embedding | Compose behavior | Hiding receiver rules behind opaque types |
| Generics on functions | Algorithms over types | Need virtual method dispatch |
FAQs
What is a method set?
The set of methods attached to a type used for interface satisfaction.
T and *T have different sets when pointer-only methods exist.
Should I default to pointer receivers?
Many teams default to *T for structs to leave room for mutation and avoid copy surprises.
Value receivers remain correct for tiny immutable types.
Why does Go auto take address for pointer receivers?
Ergonomics: v.Method() works when v is addressable and method has pointer receiver.
Non-addressable values (map elements, function results) may not qualify.
Can interfaces use value receivers only?
Yes - if all methods use T, both T and *T values satisfy the interface.
Pointer-only methods restrict satisfaction to *T.
How do receivers affect equality?
Receivers do not change struct comparison rules.
Comparable structs still compare field-wise; slices/maps inside make struct non-comparable.
Are pointer receivers less safe?
They expose shared mutation - that is intentional.
Immutability is enforced by discipline or by returning copies, not by value receivers alone.
What about methods on basic types?
Define named types: type Celsius float64 then func (c Celsius) F() Fahrenheit.
Cannot attach methods to built-in int directly.
How does receiver choice affect benchmarks?
Large struct value receivers copy on every call.
Profile before micro-optimizing; pointer receivers usually win for big structs.
Should HTTP handlers use pointer receivers?
Common pattern: func (s *Server) ServeHTTP(...) sharing dependencies on Server.
Small stateless handlers may be plain functions.
How do kubebuilder/controller-runtime types choose receivers?
Reconcilers are structs with pointer receivers mutating state and calling client.Client.
Follow generated scaffold conventions for interface compliance.
Can I mix exported pointer and unexported value methods?
Possible but confusing for interface design.
Prefer consistent exported receiver style.
Does go vet warn about receiver mismatches?
Tools like staticcheck flag copied mutexes and inconsistent receiver names.
Run golangci-lint in CI.
Related
- Functions & Methods Basics - first value and pointer method examples
- Defining and Implementing Interfaces - method sets and interface checks
- Nil Interface vs Nil Pointer - pointer methods on nil receivers
- Functions and Interfaces: Go's Composition Model - composition without inheritance
- Functions, Methods & Interfaces Best Practices - receiver consistency rules
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).