Methods (value vs pointer receivers)

Reviewed & published by Brayan K

A method is a function bound to a type — it's how you attach behaviour to your data. The single most important decision is the receiver: a value receiver works on a copy, a pointer receiver works on the original. Get that right and your types behave exactly as you intend; this lesson makes the choice clear.

Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Defining a Method

A method is just a function with an extra receiver written in parentheses before the name: func (r Rectangle) Area() float64. Inside the method, the receiver (r) gives you access to the value's fields. You call it with familiar dot notation — box.Area() — which keeps related data and behaviour neatly together.

package main

import "fmt"

type Rectangle struct {
    Width, Height float64
}

// A METHOD is a function with a RECEIVER — the (r Rectangle) part
// before the name. It "belongs to" the Rectangle type.
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

func main() {
    box := Rectangle{Width: 4, Height: 3}

    // Call a method with dot notation, like a field.
    fmt.Println("area:", box.Area())           // area: 12
    fmt.Println("perimeter:", box.Perimeter()) // perimeter: 14
}

2️⃣ Value vs Pointer Receivers

This is the heart of the lesson. A value receiver ((c Counter)) operates on a copy — any changes it makes are thrown away when the method returns. A pointer receiver ((c *Counter)) operates on the original, so mutations stick. The good news: Go automatically takes the address for you, so you still call c.Inc() even though Inc has a pointer receiver.

package main

import "fmt"

type Counter struct {
    Count int
}

// VALUE receiver: gets a COPY. Changes don't stick to the original.
func (c Counter) IncBroken() {
    c.Count++ // edits the copy only
}

// POINTER receiver: gets the address. Changes DO stick.
func (c *Counter) Inc() {
    c.Count++ // edits the original through the pointer
}

func main() {
    c := Counter{}

    c.IncBroken()
    c.IncBroken()
    fmt.Println("after IncBroken:", c.Count) // after IncBroken: 0  (!)

    // Go auto-takes the address for pointer-receiver methods,
    // so you still just write c.Inc(), not (&c).Inc().
    c.Inc()
    c.Inc()
    c.Inc()
    fmt.Println("after Inc:", c.Count) // after Inc: 3
}

3️⃣ Methods on Any Named Type

Methods aren't limited to structs — you can attach them to any named type you define, like type Celsius float64. One special method is worth knowing now: define String() string on your type and the fmt package will use it automatically whenever it prints your value. This is your first taste of how Go's interfaces work under the hood.

package main

import "fmt"

// You can attach methods to ANY named type you define,
// not just structs.
type Celsius float64

func (c Celsius) ToF() float64 {
    return float64(c)*9/5 + 32
}

// A method named String() with this signature makes your type
// print nicely — fmt looks for it automatically.
func (c Celsius) String() string {
    return fmt.Sprintf("%.1f°C", float64(c))
}

func main() {
    temp := Celsius(20)
    fmt.Println("in F:", temp.ToF()) // in F: 68
    fmt.Println(temp)                // 20.0°C  (uses String())
}

Your turn — give Circle an Area method.

package main

import "fmt"

type Circle struct {
    Radius float64
}

// 🎯 YOUR TURN — give Circle an Area method.
func (___ Circle) Area() float64 {   // 👉 receiver name, e.g. c
    return 3.14159 * c.Radius * c.Radius
}

func main() {
    c := Circle{Radius: 2}
    fmt.Printf("area: %.2f\n", c.___())  // 👉 Area

    // ✅ Expected output:  area: 12.57
}

Now make a method that actually changes the wallet — which receiver do you need?

package main

import "fmt"

type Wallet struct {
    Cash int
}

// 🎯 YOUR TURN — make Add modify the original wallet.
func (w ___Wallet) Add(n int) {   // 👉 *   (pointer receiver)
    w.Cash += n
}

func main() {
    wallet := Wallet{Cash: 10}
    wallet.Add(5)
    fmt.Println("cash:", wallet.Cash)

    // ✅ Expected output:  cash: 15
}

🧩 Reorder Challenge

These lines define a pointer-receiver method and call it, but they're scrambled. Put them in order so that, with a type Counter struct { Count int } already declared, the code compiles and c.Count ends at 1.

func (c *Counter) Inc() {
    c.Inc()
}
    c.Count++
    c := Counter{}

Why: the method declaration is one unit — its header func (c *Counter) Inc() {, the body c.Count++, and the closing brace must sit together. Separately, inside main you must create the value (c := Counter{}) before calling c.Inc(), which increments Count from 0 to 1 through the pointer.

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

GoalSyntax
Value-receiver methodfunc (r T) M() { }
Pointer-receiver methodfunc (r *T) M() { }
Call a methodv.M()
Method on named typetype C float64; func (c C) F()
Custom printfunc (t T) String() string

Rule of thumb: mutates state or large struct → pointer receiver; small, read-only → value receiver (but stay consistent across the type).

🧠 Quick Recall

Predict the output before you reveal the answer.

func (c Counter) Inc() { c.Count++ }; c := Counter{}; c.Inc(); fmt.Println(c.Count)

0 — a value receiver gets a copy, so Count++ never touches the original. Use (c *Counter) to make it 1.

func (c *Counter) Inc() { c.Count++ }; c := Counter{}; c.Inc(); c.Inc(); fmt.Println(c.Count)

2 — a pointer receiver mutates the original, and Go auto-takes the address, so both calls stick.

type C float64; func (c C) String() string { return "cold" }; fmt.Println(C(5))

cold — because C has a String() method, fmt.Println calls it instead of printing the raw number.

Frequently Asked Questions

Q: Value receiver or pointer receiver — how do I choose?

Use a pointer receiver if the method modifies the value or if the struct is large (to avoid copying). Use a value receiver for small, immutable types. When in doubt, prefer a pointer receiver — and keep all methods on a type consistent.

Q: Do I have to write (&c).Inc() for pointer methods?

No — Go automatically takes the address of an addressable value, so c.Inc() just works. The convenience mirrors how c.Field works through a struct pointer.

Q: What makes String() special?

It satisfies the fmt.Stringer interface, so the fmt package calls it automatically when printing your value. It's a preview of how interfaces let types opt into behaviour — your next lesson.

Mini-Challenge: Bank Account Methods

Add Deposit and Withdraw methods. They must change the balance, so think carefully about the receiver type. Withdraw should refuse when funds are short. Write it yourself and match the example output.

package main

import "fmt"

type BankAccount struct {
    Balance int
}

// 🎯 MINI-CHALLENGE: Deposit & Withdraw methods
// 1. Add a Deposit(amount int) method that increases Balance.
//    (Which receiver type lets the change stick? Pointer!)
// 2. Add a Withdraw(amount int) method that decreases Balance,
//    but only if there are enough funds — otherwise leave it
//    unchanged and print "insufficient funds".
//
// ✅ Example output:
//    balance: 80
//    insufficient funds
//    balance: 80

func main() {
    acc := BankAccount{Balance: 100}
    acc.Deposit(0)      // (use as needed)
    acc.Withdraw(20)    // ok -> 80
    fmt.Println("balance:", acc.Balance)
    acc.Withdraw(500)   // too much -> message, no change
    fmt.Println("balance:", acc.Balance)
}

🎉 Lesson Complete!

Practice quiz

What is a Go method?

  • A standalone function
  • A function with a receiver
  • A struct field
  • A package

Answer: A function with a receiver. A method is a function with a receiver argument, called with dot notation.

What does a value receiver operate on?

  • The original value
  • A copy of the value
  • A pointer always
  • Nothing

Answer: A copy of the value. A value receiver gets a copy, so mutations inside it do not affect the original.

Which receiver type lets a method mutate the original?

  • Value receiver
  • Pointer receiver
  • Neither
  • Both equally

Answer: Pointer receiver. A pointer receiver (*T) can modify the value the caller holds.

Given func (c Counter) Inc(), does calling c.Inc() change c?

  • Yes
  • No, it works on a copy
  • Only sometimes
  • It panics

Answer: No, it works on a copy. A value receiver copies c, so Inc() cannot change the caller's variable.

Given func (c *Counter) Inc(), can you call it on an addressable value c?

  • No, only on pointers
  • Yes — Go auto-takes &c
  • Only with new()
  • Only inside main

Answer: Yes — Go auto-takes &c. Go automatically takes the address, so c.Inc() works for a pointer-receiver method on an addressable c.

Can methods be defined on types other than structs?

  • No, structs only
  • Yes, on any named type
  • Only on slices
  • Only on interfaces

Answer: Yes, on any named type. Methods can attach to any named type, including types based on int, string, slices, etc.

Which method customises how fmt prints a type?

  • Print()
  • String() string
  • Format()
  • ToString()

Answer: String() string. Implementing String() string (fmt.Stringer) controls how fmt prints the value.

Why prefer a pointer receiver for a method that updates a field?

  • It is faster to type
  • So the change persists on the original
  • It is required by Go
  • To avoid imports

Answer: So the change persists on the original. Only a pointer receiver writes back to the caller's value; a value receiver edits a throwaway copy.

What is the receiver in func (c Counter) Inc()?

  • Inc
  • Counter
  • c of type Counter
  • c of type *Counter

Answer: c of type Counter. The receiver is c, whose type is Counter (a value receiver).

How is a method called on a variable c?

  • call(c, Inc)
  • Inc(c)
  • c.Inc()
  • c->Inc()

Answer: c.Inc(). Methods use dot notation: c.Inc().

Continue this course

Related lessons