Skip to content

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:

// These do NOT compile:
Rect{3, 4}.DoubleWidth()       // temporary is not addressable
m := map[string]Rect{"a": {3, 4}}
m["a"].DoubleWidth()            // map values are not addressable


Choosing Between Value and Pointer Receivers

Rules of Thumb

  1. If the method mutates the receiver → pointer receiver
  2. If the struct is large (many fields, large arrays) → pointer receiver (avoid copy overhead)
  3. If the struct contains fields that shouldn't be copied (sync.Mutex, sync.WaitGroup) → pointer receiver
  4. If the struct is small and immutable (like time.Time, Point) → value receiver is fine
  5. 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:

func (l *IntList) UnsafeValue() int {
    return l.Value  // panics if l is nil
}


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

  1. Be consistent with receiver type -- if one method uses *T, all methods on T should use *T
  2. Prefer pointer receivers for structs that are mutated, large, or contain sync primitives
  3. Use value receivers for small, immutable types like time.Time, Color, Point
  4. Name receivers consistently -- use a short (1-2 letter) abbreviation of the type name: func (s *Server) Start()
  5. Don't name receivers this or self -- this is non-idiomatic Go
  6. Use method expressions when you need to pass a method as a function value with an explicit receiver
  7. 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!
Use a pointer receiver (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
Pointer-receiver methods are only in the method set of *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
Workaround: store pointers in the map (map[string]*Counter) or copy-modify-assign.

Mixing Receiver Types

func (c Circle) Area() float64   { ... }
func (c *Circle) Scale(f float64) { ... }
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 T includes only value-receiver methods; *T includes 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 this or self -- use short, consistent abbreviations
  • Methods on function types enable powerful patterns like http.HandlerFunc