26. Pointer receivers

📖 Reading · 10 min
💡 Every code box below is live — edit it and hit Run.

The last lesson ended with a method that couldn't change anything. The fix is one character: put a * in front of the receiver type, and the method operates on the original instead of a copy.

The fix

package main

import "fmt"

type Counter struct {
    N int
}

func (c Counter) IncBroken() {
    c.N++
}

func (c *Counter) Inc() {
    c.N++
}

func main() {
    c := Counter{}

    c.IncBroken()
    c.IncBroken()
    fmt.Println("value receiver:", c.N)

    c.Inc()
    c.Inc()
    fmt.Println("pointer receiver:", c.N)
}

(c *Counter) means the receiver is the address of a Counter, not a copy of one. Writing c.N++ reaches through that address and modifies the real thing.

Note what you didn't have to write: no (&c).Inc(), no (*c).N++. Go inserts the address-of and the dereference for you on both sides. If a variable is addressable, calling a pointer method on it just works.

Building something up

The natural shape for a type that accumulates state:

package main

import (
    "fmt"
    "strings"
)

type Cart struct {
    Items []string
    Total float64
}

func (c *Cart) Add(item string, price float64) {
    c.Items = append(c.Items, item)
    c.Total += price
}

func (c *Cart) Clear() {
    c.Items = nil
    c.Total = 0
}

func (c Cart) Summary() string {
    if len(c.Items) == 0 {
        return "cart is empty"
    }
    return fmt.Sprintf("%d items (%s) = $%.2f",
        len(c.Items), strings.Join(c.Items, ", "), c.Total)
}

func main() {
    var cart Cart

    cart.Add("keyboard", 49.99)
    cart.Add("mouse", 25.50)
    fmt.Println(cart.Summary())

    cart.Clear()
    fmt.Println(cart.Summary())
}

Add and Clear mutate, so they take *Cart. Summary only reads... and yet you'll see in a moment why it should probably be *Cart too.

Also note var cart Cart needed no constructor — the zero value has a nil Items slice, and append to nil works fine. That's the "useful zero value" principle paying off.

The consistency rule

If any method of a type needs a pointer receiver, give all of them pointer receivers.

This is the rule that matters most in practice. Mixing them causes two real problems:

package main

import "fmt"

type Temp struct {
    Deg float64
}

func (t *Temp) Set(d float64) { t.Deg = d }
func (t Temp) Get() float64   { return t.Deg }

func main() {
    temps := []Temp{{Deg: 10}, {Deg: 20}}

    for i := range temps {
        temps[i].Set(temps[i].Get() + 5)
    }
    fmt.Println(temps)

    for _, t := range temps {
        t.Set(100)
    }
    fmt.Println(temps)
}

The second loop silently does nothing: t is a copy, so t.Set(100) sets a value that's thrown away at the end of the iteration. Ranging by index and indexing back in (temps[i]) is the fix — and one of the reasons Go programmers write for i := range more often than you'd expect.

The second problem is bigger and shows up in the next module: a []Temp of values does not satisfy an interface whose methods have pointer receivers, while a []*Temp does. Consistency saves you from ever having to think about that.

When a value receiver is right

Use a value receiver when all of these hold:

  • the method doesn't modify the receiver,
  • the type is small (a couple of fields, or a named primitive),
  • no other method on the type needs a pointer.

time.Time, Point, and most small immutable value types work this way. If you're unsure, use a pointer receiver — it's the safer default, it avoids copying, and it's what most real Go codebases do for structs.

Nil receivers are legal

A pointer receiver can be nil, and calling a method on it is not automatically a crash:

package main

import "fmt"

type List struct {
    Value int
    Next  *List
}

func (l *List) Len() int {
    if l == nil {
        return 0
    }
    return 1 + l.Next.Len()
}

func main() {
    var empty *List
    fmt.Println(empty.Len())

    l := &List{Value: 1, Next: &List{Value: 2, Next: nil}}
    fmt.Println(l.Len())
}

empty.Len() runs a method on a nil pointer and returns 0 — because the method checks. Only dereferencing nil panics, and l == nil doesn't dereference. Recursive structures lean on this constantly: the nil pointer is the empty list, and you don't need a special case anywhere else.

& — taking an address explicitly

Sometimes you want the pointer itself, usually to avoid copying a struct around:

package main

import "fmt"

type Server struct {
    Host string
    Port int
}

func (s *Server) Addr() string {
    return fmt.Sprintf("%s:%d", s.Host, s.Port)
}

func main() {
    s := &Server{Host: "localhost", Port: 8080}

    fmt.Println(s.Addr())
    fmt.Println(s.Host)

    s.Port = 9090
    fmt.Println(s.Addr())
}

&Server{...} creates the struct and gives you its address in one step. Note that s.Host works on a pointer without any dereference syntax — Go does it for you. Passing s to a function passes 8 bytes, not the whole struct, and that function can modify it.

The Pointers & Memory module goes further into what * and & really mean. For structs and methods, the two rules above cover nearly everything you'll write.

Your turn

Give BankAccount a Deposit and a Withdraw method. Withdraw should refuse (do nothing) if there aren't enough funds. The program should print:

150
150
50
package main

import "fmt"

type BankAccount struct {
    Balance int
}

// add Deposit(amount int) and Withdraw(amount int)

func main() {
    acc := BankAccount{Balance: 100}
    acc.Deposit(50)
    fmt.Println(acc.Balance)
    acc.Withdraw(500)
    fmt.Println(acc.Balance)
    acc.Withdraw(100)
    fmt.Println(acc.Balance)
}
package main

import "fmt"

type BankAccount struct {
    Balance int
}

func (a *BankAccount) Deposit(amount int) {
    a.Balance += amount
}

func (a *BankAccount) Withdraw(amount int) {
    if amount > a.Balance {
        return
    }
    a.Balance -= amount
}

func main() {
    acc := BankAccount{Balance: 100}
    acc.Deposit(50)
    fmt.Println(acc.Balance)
    acc.Withdraw(500)
    fmt.Println(acc.Balance)
    acc.Withdraw(100)
    fmt.Println(acc.Balance)
}

A silent return on a failed withdrawal is unsatisfying — the caller has no idea it failed. Module 7 fixes that properly. First: how Go programs create and validate their structs.