Methods and Receiver Functions Intermediate¶
Introduction¶
Methods in Go are functions with a receiver argument that binds them to a type. Unlike OOP languages with classes, Go attaches methods to any named type -- structs, custom int types, even function types. The choice between value receivers and pointer receivers has deep implications for mutation, interface satisfaction, and performance. Method sets -- the rules governing which methods are available on a type vs its pointer -- are a critical interview topic that trips up experienced developers.
Defining Methods¶
type Circle struct {
Radius float64
}
// Value receiver -- operates on a copy
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// Pointer receiver -- operates on the original
func (c *Circle) Scale(factor float64) {
c.Radius *= factor
}
func main() {
c := Circle{Radius: 5}
fmt.Println(c.Area()) // 78.54...
c.Scale(2)
fmt.Println(c.Radius) // 10
}
Value Receivers vs Pointer Receivers¶
type Point struct {
X, Y float64
}
// Value receiver: receives a COPY of Point
func (p Point) Distance(q Point) float64 {
dx := p.X - q.X
dy := p.Y - q.Y
return math.Sqrt(dx*dx + dy*dy)
}
// Pointer receiver: receives a POINTER to the original Point
func (p *Point) Translate(dx, dy float64) {
p.X += dx // modifies the original
p.Y += dy
}
| Aspect | Value Receiver (t T) |
Pointer Receiver (t *T) |
|---|---|---|
| Mutation | Cannot modify original | Can modify original |
| Copy | Receives a full copy | Receives a pointer (8 bytes) |
| Nil safety | Cannot be called on nil | Can be called on nil (handle explicitly) |
| Interface satisfaction | Available on both T and *T |
Available only on *T |
Method Sets: The Critical Rule¶
The method set of a type determines which interfaces it can satisfy. This is the most important and most tested concept.
type Sizer interface {
Size() int
}
type Resizer interface {
Resize(n int)
}
type Buffer struct {
data []byte
}
func (b Buffer) Size() int { return len(b.data) } // value receiver
func (b *Buffer) Resize(n int) { b.data = make([]byte, n) } // pointer receiver
The Rule¶
| Type | Method Set |
|---|---|
T (value) |
Only methods with value receivers |
*T (pointer) |
Methods with both value and pointer receivers |
var b Buffer = Buffer{data: make([]byte, 10)}
// Value type -- only value-receiver methods in method set
var s Sizer = b // OK: Buffer has Size() (value receiver)
// var r Resizer = b // COMPILE ERROR: Buffer's method set lacks Resize()
// Pointer type -- has ALL methods in method set
var s2 Sizer = &b // OK: *Buffer has Size()
var r2 Resizer = &b // OK: *Buffer has Resize()
Why This Matters
You can call pointer-receiver methods on a value using a variable (b.Resize(5) works because Go auto-takes the address), but a value stored in an interface cannot have its address taken. That's why var r Resizer = b fails -- the compiler can't guarantee addressability of the value inside the interface.
Auto-Dereference and Auto-Address¶
Go automatically adjusts between values and pointers for method calls on variables (not interface values).
type Rect struct {
W, H float64
}
func (r Rect) Area() float64 { return r.W * r.H }
func (r *Rect) DoubleWidth() { r.W *= 2 }
r := Rect{W: 3, H: 4}
p := &r
r.Area() // OK: value method on value
r.DoubleWidth() // OK: Go auto-takes address (&r).DoubleWidth()
p.Area() // OK: Go auto-dereferences (*p).Area()
p.DoubleWidth() // OK: pointer method on pointer
The Convenience is Syntactic Only
Auto-addressing works on addressable variables. It does not work for values stored in interfaces, map values, or return values:
Choosing Between Value and Pointer Receivers¶
Rules of Thumb¶
- If the method mutates the receiver → pointer receiver
- If the struct is large (many fields, large arrays) → pointer receiver (avoid copy overhead)
- If the struct contains fields that shouldn't be copied (
sync.Mutex,sync.WaitGroup) → pointer receiver - If the struct is small and immutable (like
time.Time,Point) → value receiver is fine - Be consistent: if any method on a type uses a pointer receiver, all methods should
// Small, immutable -- value receiver is fine
type Color struct{ R, G, B uint8 }
func (c Color) Hex() string {
return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
}
// Large, mutable, has mutex -- always pointer receiver
type Cache struct {
mu sync.RWMutex
data map[string][]byte
}
func (c *Cache) Get(key string) ([]byte, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
func (c *Cache) Set(key string, val []byte) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = val
}
Methods on Non-Struct Types¶
Methods can be defined on any named type declared in the current package.
type Celsius float64
type Fahrenheit float64
func (c Celsius) ToFahrenheit() Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
func (f Fahrenheit) ToCelsius() Celsius {
return Celsius((f - 32) * 5 / 9)
}
temp := Celsius(100)
fmt.Println(temp.ToFahrenheit()) // 212
// Methods on function types (used in http.HandlerFunc pattern)
type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
Restriction
You cannot define methods on types from other packages or on built-in types directly. You must create a named type alias in your own package first.
Method Expressions¶
A method expression extracts a method from a type as a regular function, with the receiver becoming the first parameter.
type Point struct{ X, Y float64 }
func (p Point) Distance(q Point) float64 {
return math.Hypot(p.X-q.X, p.Y-q.Y)
}
// Method expression: Point.Distance is func(Point, Point) float64
dist := Point.Distance
fmt.Println(dist(Point{0, 0}, Point{3, 4})) // 5
// Useful with higher-order functions
points := []Point{{1, 1}, {3, 4}, {0, 0}}
sort.Slice(points, func(i, j int) bool {
origin := Point{0, 0}
return Point.Distance(points[i], origin) < Point.Distance(points[j], origin)
})
Method Values¶
A method value binds a method to a specific receiver instance, producing a closure.
type Logger struct {
Prefix string
}
func (l *Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.Prefix, msg)
}
logger := &Logger{Prefix: "APP"}
logFn := logger.Log // method value: bound to this specific logger
logFn("starting") // "[APP] starting"
logFn("ready") // "[APP] ready"
// Useful for callbacks and goroutines
go logger.Log("background task started")
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
logger.Log("health check")
w.WriteHeader(200)
})
Nil Receivers¶
Methods with pointer receivers can be called on nil pointers. This enables useful patterns but requires care.
type IntList struct {
Value int
Next *IntList
}
// Handles nil gracefully -- useful for linked data structures
func (l *IntList) Sum() int {
if l == nil {
return 0
}
return l.Value + l.Next.Sum()
}
var head *IntList
fmt.Println(head.Sum()) // 0 -- no panic
Nil Receiver Panics
If a pointer-receiver method accesses fields without checking for nil, it panics:
Quick Reference¶
| Concept | Syntax | Notes |
|---|---|---|
| Value receiver | func (t T) M() |
Operates on copy; in method set of both T and *T |
| Pointer receiver | func (t *T) M() |
Operates on original; in method set of *T only |
| Method call | v.Method() |
Auto-addresses/dereferences as needed |
| Method expression | T.Method |
func(T, args...) ret -- receiver becomes first param |
| Method value | v.Method |
Closure bound to specific receiver |
| Method on non-struct | type MyInt int; func (m MyInt) M() |
Works on any named type in current package |
| Nil pointer receiver | func (t *T) M() with t == nil |
Legal but must handle nil explicitly |
Best Practices¶
- Be consistent with receiver type -- if one method uses
*T, all methods onTshould use*T - Prefer pointer receivers for structs that are mutated, large, or contain sync primitives
- Use value receivers for small, immutable types like
time.Time,Color,Point - Name receivers consistently -- use a short (1-2 letter) abbreviation of the type name:
func (s *Server) Start() - Don't name receivers
thisorself-- this is non-idiomatic Go - Use method expressions when you need to pass a method as a function value with an explicit receiver
- Document nil-receiver behavior if your method handles nil gracefully
Common Pitfalls¶
Value Receiver Doesn't Mutate
func (u User) SetName(name string) {
u.Name = name // modifies the COPY -- original is unchanged
}
u := User{Name: "Alice"}
u.SetName("Bob")
fmt.Println(u.Name) // "Alice" -- unchanged!
(u *User) when the method needs to modify the struct.
Method Set and Interface Satisfaction
type Saver interface { Save() error }
func (d *Doc) Save() error { /* ... */ }
var s Saver = Doc{} // COMPILE ERROR: Doc doesn't have Save in its method set
var s Saver = &Doc{} // OK: *Doc has Save
*T, not T. This is the #1 cause of "X does not implement Y" errors.
Map Values Are Not Addressable
type Counter struct{ N int }
func (c *Counter) Increment() { c.N++ }
m := map[string]Counter{"a": {N: 0}}
m["a"].Increment() // COMPILE ERROR: cannot take address of map value
map[string]*Counter) or copy-modify-assign.
Mixing Receiver Types
This works but violates consistency. Convention says: if any method needs a pointer receiver, use pointer receivers for all methods on that type.Performance Considerations¶
| Scenario | Recommendation |
|---|---|
| Small struct (≤3 fields, no pointers) | Value receiver is efficient -- fits in registers, no heap escape |
| Large struct (many fields or large arrays) | Pointer receiver avoids expensive copies |
Struct with sync.Mutex or channels |
Must use pointer receiver -- copying sync primitives is undefined behavior |
| Hot-path method calls | Value receivers can avoid heap allocation; pointer receivers may cause escape |
| Interface dispatch | Adds ~1-2 ns indirection; direct method calls are faster |
Escape Analysis Impact
Using a pointer receiver doesn't automatically cause heap allocation. The Go compiler's escape analysis determines whether *T escapes to the heap. Use go build -gcflags='-m' to inspect escape decisions.
Interview Tips¶
Interview Tip
When asked "What's the difference between value and pointer receivers?", cover three dimensions: mutation (pointer can modify, value can't), copy cost (value copies the whole struct, pointer copies 8 bytes), and method set (pointer-receiver methods only in *T's method set).
Interview Tip
The method set rule is a top interview question: "Why can't I assign my struct value to this interface?" Answer: pointer-receiver methods are excluded from the value type's method set because the compiler can't take the address of a value inside an interface.
Interview Tip
Know why Go doesn't use this or self: Go treats the receiver as an explicit parameter, making it clear that it's just a regular argument. The short naming convention (e.g., s for *Server) reinforces that methods are simply functions with a receiver.
Interview Tip
If asked about the http.HandlerFunc pattern, explain: it's a method on a function type that makes plain functions satisfy the http.Handler interface. This adapter pattern is a powerful illustration of Go's method system.
Key Takeaways¶
- Methods are functions with a receiver argument that can be defined on any named type
- Value receivers operate on a copy; pointer receivers operate on the original
- The method set of
Tincludes only value-receiver methods;*Tincludes both - Method set rules directly determine interface satisfaction -- this is critical
- Go auto-addresses and auto-dereferences for method calls on variables (not interface values)
- Be consistent: if one method uses a pointer receiver, all should
- Method expressions turn methods into regular functions; method values create bound closures
- Never name receivers
thisorself-- use short, consistent abbreviations - Methods on function types enable powerful patterns like
http.HandlerFunc