βš™οΈ Chapter 10 of 20

Methods

Methods are functions with a receiver. Go's method system is orthogonal β€” you can add methods to any named type in your package. Understanding the difference between value and pointer receivers is fundamental to writing correct Go code.

⏱ β€”πŸ“Š Intermediate

🎯 Learning Objectives

βœ… After this chapter you will:
  • Define methods with both value and pointer receivers
  • Explain the critical difference: which receiver type modifies the original
  • Understand method sets and why they matter for interfaces
  • Add methods to non-struct types (int, string, etc.)
  • Use method values and method expressions
  • Implement the fmt.Stringer interface
  • Understand how methods are dispatched at runtime

πŸ“ Method Syntax

A method is a function with a special first parameter: the receiver. The receiver appears between func and the method name.

go
package main

import (
    "fmt"
    "math"
)

// Type definition
type Circle struct {
    X, Y   float64  // centre
    Radius float64
}

// Value receiver β€” receives a COPY of the circle
// Use for: read-only operations, small structs, basic getters
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// Pointer receiver β€” receives a POINTER to the circle
// Use for: mutations, large structs, implementing interfaces that need mutation
func (c *Circle) Scale(factor float64) {
    c.Radius *= factor  // modifies the original circle
}

func (c *Circle) MoveTo(x, y float64) {
    c.X = x
    c.Y = y
}

// String() method β€” implements fmt.Stringer interface
// fmt.Println will use this automatically
func (c Circle) String() string {
    return fmt.Sprintf("Circle{centre:(%.2f,%.2f) r=%.2f}", c.X, c.Y, c.Radius)
}

// Methods on non-struct types β€” any named type in the same 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)
}

func (c Celsius) String() string {
    return fmt.Sprintf("%.1fΒ°C", float64(c))
}

func main() {
    c := Circle{X: 0, Y: 0, Radius: 5}
    fmt.Println(c)                           // Circle{centre:(0.00,0.00) r=5.00}
    fmt.Printf("Area: %.2f\n", c.Area())    // Area: 78.54
    fmt.Printf("Perim: %.2f\n", c.Perimeter())

    c.Scale(2)    // pointer receiver β€” modifies c
    fmt.Println("after Scale(2):", c)  // r=10.00

    c.MoveTo(3, 4)
    fmt.Println("after MoveTo:", c)  // centre:(3.00,4.00)

    // Value receiver called on pointer β€” Go auto-dereferences
    p := &Circle{Radius: 7}
    fmt.Printf("p.Area() = %.2f\n", p.Area())  // works! auto-dereferences

    // Pointer receiver called on addressable value β€” Go auto-addresses
    // c.Scale(1.5) β€” c is addressable, so Go passes &c automatically

    temp := Celsius(100)
    fmt.Println(temp)                     // 100.0Β°C (uses String())
    fmt.Println(temp.ToFahrenheit())      // 212
}
βœ… Output
Circle{centre:(0.00,0.00) r=5.00} Area: 78.54 Perim: 31.42 after Scale(2): Circle{centre:(0.00,0.00) r=10.00} after MoveTo: Circle{centre:(3.00,4.00) r=10.00} p.Area() = 153.94 100.0Β°C 212

βš–οΈ Value vs Pointer Receivers β€” The Decision

QuestionValue ReceiverPointer Receiver
Does the method modify the receiver?NoYes β€” must use pointer
Is the struct large?No (use value, cheaper to read)Yes (pointer avoids copying)
Does the type have sync.Mutex or similar?Never β€” mutex must not be copiedYes β€” must use pointer
Does other code call it on non-addressable values?FineCannot (see method sets below)
Is consistency with other methods important?Use one or the other consistentlyMix is confusing β€” pick one style
πŸ“Œ The Golden Rule

If any method on a type needs a pointer receiver, then all methods should use pointer receivers. Mixing value and pointer receivers on the same type is confusing and creates inconsistent interface satisfaction.

go
package main

import "fmt"

type Counter struct {
    count int
}

// ❌ WRONG: value receiver β€” modifies a copy, not the original!
func (c Counter) BadIncrement() {
    c.count++  // modifies the LOCAL COPY, not the original Counter
}

// βœ… CORRECT: pointer receiver β€” modifies the original
func (c *Counter) Increment() {
    c.count++
}

func (c *Counter) Add(n int) {
    c.count += n
}

func (c Counter) Value() int {  // value receiver OK for read-only
    return c.count
}

func (c *Counter) Reset() {
    c.count = 0
}

func main() {
    c := Counter{count: 0}

    c.BadIncrement()  // does nothing!
    fmt.Println("after BadIncrement:", c.Value())  // 0

    c.Increment()
    c.Increment()
    c.Add(10)
    fmt.Println("after Increment x2 + Add(10):", c.Value())  // 12

    c.Reset()
    fmt.Println("after Reset:", c.Value())  // 0
}
βœ… Output
after BadIncrement: 0 after Increment x2 + Add(10): 12 after Reset: 0

πŸ“š Method Sets

The method set of a type determines which interfaces it satisfies. This is a subtle but important concept.

TypeMethod Set includes
T (value)All methods with value receiver (func (t T))
*T (pointer)All methods with value receiver AND pointer receiver (func (t *T))
go
package main

import "fmt"

type Writer interface {
    Write(data string) error
}

type Reader interface {
    Read() (string, error)
}

type ReadWriter interface {
    Reader
    Writer
}

type Buffer struct {
    data string
}

// Pointer receiver β€” only *Buffer implements Writer
func (b *Buffer) Write(data string) error {
    b.data += data
    return nil
}

// Value receiver β€” both Buffer and *Buffer implement Reader
func (b Buffer) Read() (string, error) {
    return b.data, nil
}

func useWriter(w Writer) {
    w.Write("hello")
}

func main() {
    b := Buffer{}
    pb := &Buffer{}

    // *Buffer has method set: {Write, Read} β€” satisfies ReadWriter
    var rw ReadWriter = pb  // βœ“ works
    rw.Write("world")

    // Buffer (value) has method set: {Read} only β€” CANNOT satisfy Writer
    // var w Writer = b  // ← compile error: Buffer does not implement Writer
    _ = rw

    // But Go auto-takes address when calling pointer methods on addressable values:
    b.Write("test")   // Go automatically passes &b β€” b must be addressable!
    fmt.Println(b.Read())  // test

    useWriter(pb)  // fine β€” *Buffer implements Writer
    // useWriter(b)  // ← compile error: Buffer does not implement Writer
}
⚠️ Method Set Gotcha

A value stored in an interface cannot have its address taken β€” it's not addressable. So if you store a Buffer value in an interface variable, you cannot call pointer receiver methods through the interface. This is why you should use pointer receiver methods consistently β€” *Buffer always works.

πŸ”§ Method Values & Method Expressions

go
package main

import "fmt"

type Adder struct{ base int }

func (a Adder) Add(x int) int { return a.base + x }

func main() {
    a := Adder{base: 10}

    // Method value β€” binds the receiver
    addFn := a.Add   // addFn is func(int) int, a is already bound
    fmt.Println(addFn(5))   // 15
    fmt.Println(addFn(20))  // 30

    // Useful: pass as a callback
    numbers := []int{1, 2, 3, 4, 5}
    results := mapInts(numbers, addFn)
    fmt.Println(results)  // [11 12 13 14 15]

    // Method expression β€” receiver passed explicitly
    addExpr := Adder.Add   // addExpr is func(Adder, int) int
    fmt.Println(addExpr(a, 5))   // 15
    fmt.Println(addExpr(Adder{base: 100}, 5))  // 105
}

func mapInts(s []int, fn func(int) int) []int {
    r := make([]int, len(s))
    for i, v := range s { r[i] = fn(v) }
    return r
}
βœ… Output
15 30 [11 12 13 14 15] 15 105

🎀 Interview Questions

1
What is the difference between a value receiver and a pointer receiver?
β–Ά
Detailed Answer

Value receiver (t T): the method receives a copy of the value. Modifications inside the method do not affect the original. Safe to call on both values and pointers (Go auto-dereferences).

Pointer receiver (t *T): the method receives a pointer to the original. Modifications affect the original. Can only be called via the interface when the value is addressable.

Use pointer receivers when: (1) the method must modify the receiver; (2) the struct is large (avoid copy cost); (3) the struct contains a mutex (must not be copied). In practice, most types in Go use pointer receivers for consistency.

Automatic conversion: Go automatically takes the address of an addressable value to call a pointer method (v.Method() β†’ (&v).Method()). But this doesn't apply when the value is stored in an interface (not addressable).

2
What is the method set of T and *T? Why does this matter for interfaces?
β–Ά
Detailed Answer

Method set of T (value): includes only methods with value receivers.

Method set of *T (pointer): includes methods with BOTH value and pointer receivers.

This matters for interfaces because interface satisfaction is determined by method sets. If an interface requires a method with a pointer receiver, only *T (not T) satisfies it. This is why: (1) you should almost always pass *T to functions expecting an interface; (2) storing T in an interface and calling pointer methods through it causes a compile error.

Common mistake: implementing all methods with pointer receivers but trying to store a value (not pointer) in an interface variable. Fix: always use *T with interfaces when you have pointer receiver methods.

πŸ§ͺ Quiz

🧩
Methods
4 questions
1. After calling a value receiver method that does c.count++, the original count is:
2. Which type has a larger method set: T or *T?
3. Can you define a method on a type like type MyInt int?
4. Implementing which method makes fmt.Println use your custom output format?
β€”

πŸ“Œ Cheat Sheet

πŸ“Œ Methods β€” Quick Reference
Value receiver
func (t T) Method() { }
Pointer receiver
func (t *T) Mutate() { t.x++ }
Stringer
func (t T) String() string { }
Method set T
Only value receiver methods
Method set *T
Value + pointer receiver methods
Auto-address
v.PtrMethod() β†’ (&v).PtrMethod()
Non-struct method
type C float64; func (c C) F() { }
Method value
fn := obj.Method // bound receiver
Method expression
fn := T.Method // receiver as arg
Mutation
Always use pointer receiver!