Methods: Value vs Pointer Receivers
Methods attach behavior to types through receivers - either a copy (T) or a pointer (*T).
Search across all documentation pages
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.
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.
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:
Inc, Write, SetState).fmt.Stringer on large structs is fine either way; sync.Mutex must not copy).time.Time uses value methods).T and *T assignments.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:
Credit mutates balance.Balance returns a snapshot without exposing mutable state.Creditor requires Credit; satisfied by *Account, not Account value.*Account so callers land on the pointer method set.a.Credit(x) becomes Credit(a, x) with receiver as first argument.T: methods with receiver T*T: methods with receiver T and *T| 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 |
| Method receiver | var x T satisfies? | var p *T satisfies? |
|---|---|---|
func (T) M() | Yes | Yes |
func (*T) M() | No | Yes |
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.
*T or return updated copy explicitly.sync.Mutex and panic. Fix: pointer receivers only; sometimes unexport struct.func (T) Foo and func (*T) Bar breaks T interface assignments. Fix: unify on *T when any pointer method exists.var p *T; p.Method() if method handles nil (see nil guards in stdlib). Fix: document or panic early for invalid nil.Account value to Creditor fails. Fix: store *Account or add value-receiver wrappers only when semantically sound.*bytes.Buffer.| 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 |
The set of methods attached to a type used for interface satisfaction.
T and *T have different sets when pointer-only methods exist.
Many teams default to *T for structs to leave room for mutation and avoid copy surprises.
Value receivers remain correct for tiny immutable types.
Ergonomics: v.Method() works when v is addressable and method has pointer receiver.
Non-addressable values (map elements, function results) may not qualify.
Yes - if all methods use T, both T and *T values satisfy the interface.
Pointer-only methods restrict satisfaction to *T.
Receivers do not change struct comparison rules.
Comparable structs still compare field-wise; slices/maps inside make struct non-comparable.
They expose shared mutation - that is intentional.
Immutability is enforced by discipline or by returning copies, not by value receivers alone.
Define named types: type Celsius float64 then func (c Celsius) F() Fahrenheit.
Cannot attach methods to built-in int directly.
Large struct value receivers copy on every call.
Profile before micro-optimizing; pointer receivers usually win for big structs.
Common pattern: func (s *Server) ServeHTTP(...) sharing dependencies on Server.
Small stateless handlers may be plain functions.
Reconcilers are structs with pointer receivers mutating state and calling client.Client.
Follow generated scaffold conventions for interface compliance.
Possible but confusing for interface design.
Prefer consistent exported receiver style.
Tools like staticcheck flag copied mutexes and inconsistent receiver names.
Run golangci-lint in CI.
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).
Reviewed by Chris St. John·Last updated Jul 18, 2026