26. Pointer receivers
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.