tl;dr:
- Methods using receiver pointers are common; the rule of thumb for receivers is, "If in doubt, use a pointer."
- Slices, maps, channels, strings, function values, and interface values are implemented with pointers internally, and a pointer to them is often redundant.
- Elsewhere, use pointers for big structs or structs you'll have to change, and otherwise pass values, because getting things changed by surprise via a pointer is confusing.
One case where you should often use a pointer:
- Receivers are pointers more often than other arguments. It's not unusual for methods to modify the thing they're called on, or for named types to be large structs, so the guidance is to default to pointers except in rare cases.
- Jeff Hodges' copyfighter tool automatically searches for non-tiny receivers passed by value.
- Jeff Hodges' copyfighter tool automatically searches for non-tiny receivers passed by value.
Some situations where you don't need pointers:
Code review guidelines suggest passing small structs like
type Point struct { latitude, longitude float64 }
, and maybe even things a bit bigger, as values, unless the function you're calling needs to be able to modify them in place.- Value semantics avoid aliasing situations where an assignment over here changes a value over there by surprise.
- Passing small structs by value can be more efficient by avoiding cache misses or heap allocations. In any case, when pointers and values perform similarly, the Go-y approach is to choose whatever provides the more natural semantics rather than squeeze out every last bit of speed.
- So, Go Wiki's code review comments page suggests passing by value when structs are small and likely to stay that way.
- If the "large" cutoff seems vague, it is; arguably many structs are in a range where either a pointer or a value is OK. As a lower bound, the code review comments suggest slices (three machine words) are reasonable to use as value receivers. As something nearer an upper bound,
bytes.Replace
takes 10 words' worth of args (three slices and anint
). You can find situations where copying even large structs turns out a performance win, but the rule of thumb is not to.
For slices, you don't need to pass a pointer to change elements of the array.
io.Reader.Read(p []byte)
changes the bytes ofp
, for instance. It's arguably a special case of "treat little structs like values," since internally you're passing around a little structure called a slice header (see Russ Cox (rsc)'s explanation). Similarly, you don't need a pointer to modify a map or communicate on a channel.For slices you'll reslice (change the start/length/capacity of), built-in functions like
append
accept a slice value and return a new one. I'd imitate that; it avoids aliasing, returning a new slice helps call attention to the fact that a new array might be allocated, and it's familiar to callers.- It's not always practical follow that pattern. Some tools like database interfaces or serializers need to append to a slice whose type isn't known at compile time. They sometimes accept a pointer to a slice in an
interface{}
parameter.
- It's not always practical follow that pattern. Some tools like database interfaces or serializers need to append to a slice whose type isn't known at compile time. They sometimes accept a pointer to a slice in an
Maps, channels, strings, and function and interface values, like slices, are internally references or structures that contain references already, so if you're just trying to avoid getting the underlying data copied, you don't need to pass pointers to them. (rsc wrote a separate post on how interface values are stored).
- You still may need to pass pointers in the rarer case that you want to modify the caller's struct:
flag.StringVar
takes a*string
for that reason, for example.
- You still may need to pass pointers in the rarer case that you want to modify the caller's struct:
Where you use pointers:
Consider whether your function should be a method on whichever struct you need a pointer to. People expect a lot of methods on
x
to modifyx
, so making the modified struct the receiver may help to minimize surprise. There are guidelines on when receivers should be pointers.Functions that have effects on their non-receiver params should make that clear in the godoc, or better yet, the godoc and the name (like
reader.WriteTo(writer)
).You mention accepting a pointer to avoid allocations by allowing reuse; changing APIs for the sake of memory reuse is an optimization I'd delay until it's clear the allocations have a nontrivial cost, and then I'd look for a way that doesn't force the trickier API on all users:
- For avoiding allocations, Go's escape analysis is your friend. You can sometimes help it avoid heap allocations by making types that can be initialized with a trivial constructor, a plain literal, or a useful zero value like
bytes.Buffer
. - Consider a
Reset()
method to put an object back in a blank state, like some stdlib types offer. Users who don't care or can't save an allocation don't have to call it. - Consider writing modify-in-place methods and create-from-scratch functions as matching pairs, for convenience:
existingUser.LoadFromJSON(json []byte) error
could be wrapped byNewUserFromJSON(json []byte) (*User, error)
. Again, it pushes the choice between laziness and pinching allocations to the individual caller. - Callers seeking to recycle memory can let
sync.Pool
handle some details. If a particular allocation creates a lot of memory pressure, you're confident you know when the alloc is no longer used, and you don't have a better optimization available,sync.Pool
can help. (CloudFlare published a useful (pre-sync.Pool
) blog post about recycling.)
- For avoiding allocations, Go's escape analysis is your friend. You can sometimes help it avoid heap allocations by making types that can be initialized with a trivial constructor, a plain literal, or a useful zero value like
Finally, on whether your slices should be of pointers: slices of values can be useful, and save you allocations and cache misses. There can be blockers:
- The API to create your items might force pointers on you, e.g. you have to call
NewFoo() *Foo
rather than let Go initialize with the zero value. - The desired lifetimes of the items might not all be the same. The whole slice is freed at once; if 99% of the items are no longer useful but you have pointers to the other 1%, all of the array remains allocated.
- Copying or moving the values might cause you performance or correctness problems, making pointers more attractive. Notably,
append
copies items when it grows the underlying array. Pointers to slice items from before theappend
may not point to where the item was copied after, copying can be slower for huge structs, and for e.g.sync.Mutex
copying isn't allowed. Insert/delete in the middle and sorting also move items around so similar considerations can apply.
Broadly, value slices can make sense if either you get all of your items in place up front and don't move them (e.g., no more append
s after initial setup), or if you do keep moving them around but you're confident that's OK (no/careful use of pointers to items, and items are small or you've measured the perf impact). Sometimes it comes down to something more specific to your situation, but that's a rough guide.