39. Pointer or value? Choosing deliberately
Knowing how pointers work is the easy part. The question that comes up in every code review is when to use one. Go has clear answers.
Reason 1: you need to modify the original
package main
import "fmt"
type Counter struct{ N int }
func incValue(c Counter) { c.N++ }
func incPointer(c *Counter) { c.N++ }
func main() {
c := Counter{}
incValue(c)
fmt.Println("value param:", c.N)
incPointer(&c)
fmt.Println("pointer param:", c.N)
}
If the function's job is to change the thing, it takes a pointer. This is the main reason, and it's not a judgement call — the alternative simply doesn't work.
Reason 2: the value is large
Passing a struct copies every byte of it:
package main
import "fmt"
type Big struct {
Data [1000]int
Name string
}
func byValue(b Big) int { return b.Data[0] }
func byPointer(b *Big) int { return b.Data[0] }
func main() {
b := Big{Name: "big"}
b.Data[0] = 42
fmt.Println(byValue(b))
fmt.Println(byPointer(&b))
fmt.Println("copied per call:", len(b.Data)*8, "bytes vs 8 bytes for a pointer")
}
A [1000]int is 8 KB. byValue copies all of it on every call; byPointer
copies one machine word.
The threshold in practice is lower than people expect but higher than they fear: a struct with a handful of fields is fine to copy — often faster than a pointer, since it stays on the stack and the CPU cache likes it. Somewhere around "several dozen bytes, called in a hot loop" the calculus flips. Don't guess in either direction; if it matters, benchmark it (module 12).
Reason 3: distinguishing "zero" from "not set"
A pointer can be nil, and sometimes that's exactly the information you need:
package main
import "fmt"
type Settings struct {
Timeout int
Debug *bool
}
func describe(s Settings) {
fmt.Print("timeout ", s.Timeout, ", debug ")
if s.Debug == nil {
fmt.Println("(not specified, defaulting to false)")
return
}
fmt.Println(*s.Debug, "(explicitly set)")
}
func main() {
yes := true
no := false
describe(Settings{Timeout: 30})
describe(Settings{Timeout: 30, Debug: &no})
describe(Settings{Timeout: 30, Debug: &yes})
}
With a plain bool, "the user set it to false" and "the user said nothing"
are the same value. A *bool tells them apart — which is why config parsers
and JSON APIs use pointer fields for genuinely optional values.
Use it sparingly. Every *bool field means a nil check at every use site.
When to prefer a value
Values are the better default more often than newcomers assume:
package main
import "fmt"
type Point struct{ X, Y int }
func (p Point) Add(q Point) Point {
return Point{X: p.X + q.X, Y: p.Y + q.Y}
}
func (p Point) Scale(f int) Point {
return Point{X: p.X * f, Y: p.Y * f}
}
func main() {
a := Point{1, 2}
b := Point{10, 20}
fmt.Println(a.Add(b))
fmt.Println(a.Add(b).Scale(2))
fmt.Println("a is untouched:", a)
}
Small, immutable, arithmetic-like types read beautifully as values: no nil to
check, safe to share between goroutines, and chainable because each method
returns a new value. time.Time works exactly this way.
Rules of thumb:
| use a value | use a pointer |
|---|---|
| small struct (a few fields) | the method or function mutates it |
| immutable / arithmetic type | large struct, copied in a hot path |
| you want copy semantics | the type contains a mutex or other non-copyable |
| no nil case makes sense | "unset" must differ from "zero" |
Never copy a struct containing a mutex
This one is a hard rule, not a preference:
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
n int
}
func (c *SafeCounter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}
func main() {
c := &SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Inc()
}()
}
wg.Wait()
fmt.Println(c.Value())
}
Copying a sync.Mutex copies its lock state, so the copy and the original
protect nothing. Every method takes *SafeCounter, and the value lives
behind a pointer from the moment it's created. go vet catches accidental
copies for you — which is why module 12 tells you to run it.
Consistency beats cleverness
Once a type uses pointer receivers, use pointers everywhere for it: in
slices ([]*User), in maps (map[int]*User), as parameters, as results.
Mixing forms in one codebase produces the "does not implement" errors from
module 6 and the "range gives you a copy" bug from module 5.
package main
import "fmt"
type User struct {
Name string
Visits int
}
func (u *User) Visit() { u.Visits++ }
func main() {
users := []*User{
{Name: "Ada"},
{Name: "Grace"},
}
for _, u := range users {
u.Visit()
u.Visit()
}
byName := make(map[string]*User, len(users))
for _, u := range users {
byName[u.Name] = u
}
byName["Ada"].Visit()
for _, u := range users {
fmt.Printf("%s: %d visits\n", u.Name, u.Visits)
}
}
for _, u := range users works here precisely because u is a copy of a
pointer — the copy still points at the same User. That's the whole
trick, and it's why []*T is so common in Go code.
Note also byName["Ada"].Visit() — a map of pointers lets you modify the
value in place, which a map[string]User cannot do at all (map values aren't
addressable; the compiler rejects byName["Ada"].Visits++).
Your turn
Complete applyDiscount so it reduces the price of every product in the
slice by the given percentage, in place:
Widget 90
Gadget 45
package main
import "fmt"
type Product struct {
Name string
Price int
}
// write applyDiscount(products []*Product, percent int)
func main() {
products := []*Product{
{Name: "Widget", Price: 100},
{Name: "Gadget", Price: 50},
}
applyDiscount(products, 10)
for _, p := range products {
fmt.Println(p.Name, p.Price)
}
}
package main
import "fmt"
type Product struct {
Name string
Price int
}
func applyDiscount(products []*Product, percent int) {
for _, p := range products {
p.Price = p.Price - p.Price*percent/100
}
}
func main() {
products := []*Product{
{Name: "Widget", Price: 100},
{Name: "Gadget", Price: 50},
}
applyDiscount(products, 10)
for _, p := range products {
fmt.Println(p.Name, p.Price)
}
}
Next: the two allocation built-ins, new and make.