Engineering Blog

                            

Not knowing which type of receiver to use

This post is about choosing a receiver type for a method which isn’t always straightforward. When should we use value receivers? When should we use pointer receivers? In this section, we look at the conditions to make the right decision.

How value or pointer receiver works

In many contexts, using a value or pointer receiver should be dictated not by performance but rather by other conditions. But first, let’s refresh our memories about how receivers work. In Go, we can attach either a value or a pointer receiver to a method. With a value receiver, Go makes a copy of the value and passes it to the method. Any changes to the object remain local to the method. The original object remains unchanged. As an illustration, the following example mutates a value receiver:

type customer struct {
balance float64
}
func (c customer) add(v float64) {
c.balance += v
}
func main() {
c := customer{balance: 100.}
c.add(50.)
fmt.Printf("balance: %.2f\n", c.balance)
}

Because we use a value receiver, incrementing the balance in the add method doesn’t mutate the balance field of the original customer struct and result is 100.00

On the other hand, with a pointer receiver, Go passes the address of an object to the method. Intrinsically, it remains a copy, but we only copy a pointer, not the object itself (passing by reference doesn’t exist in Go). Any modifications to the receiver are done on the original object. Here is the same example, but now the receiver is a
pointer:

type customer struct {
balance float64
}
func (c *customer) add(operation float64) {
c.balance += operation
}
func main() {
c := customer{balance: 100.0}
c.add(50.0)
fmt.Printf("balance: %.2f\n", c.balance)
}

Because we use a pointer receiver, incrementing the balance mutates the balance field of the original customer struct and result is 150.00

Choosing between value and pointer receivers

Choosing between value and pointer receivers isn’t always straightforward. Let’s discuss some of the conditions to help us choose.

A receiver must be a pointer

  • If the method needs to mutate the receiver. This rule is also valid if the receiver is a slice and a method needs to append elements:
type slice []int
func (s *slice) add(element int) {
s = append(s, element)
}
  • If the method receiver contains a field that cannot be copied.

A receiver should be a pointer

  • If the receiver is a large object. Using a pointer can make the call more efficient, as doing so prevents making an extensive copy. When in doubt about how large is large, benchmarking can be the solution; it’s pretty much impossible to state a specific size, because it depends on many factors.

A receiver must be a value

  • If we have to enforce a receiver’s immutability.
  • If the receiver is a map, function, or channel. Otherwise, a compilation error occurs.

A receiver should be a value

  • If the receiver is a slice that doesn’t have to be mutated.
  • If the receiver is a small array or struct that is naturally a value type without mutable fields, such as time.Time.
  • If the receiver is a basic type such as int, float64, or string.

One case needs more discussion. Let’s say that we design a different customer struct. Its mutable fields aren’t part of the struct directly but are inside another struct:

type customer struct {
data *data
}
type data struct {
balance float64
}
func (c customer) add(operation float64) {
c.data.balance += operation
}
func main() {
c := customer{data: &data{
balance: 100,
}}
c.add(50.)
fmt.Printf("balance: %.2f\n", c.data.balance)
}


Even though the receiver is a value, calling add changes the actual balance in the end we get 150.00

In this case, we don’t need the receiver to be a pointer to mutate balance. However, for clarity, we may favor using a pointer receiver to highlight that customer as a whole object is mutable.

Mixing receiver types

Are we allowed to mix receiver types, such as a struct containing multiple methods, some of which have pointer receivers and others of which have value receivers? The consensus tends toward forbidding it. However, there are some counterexamples in the standard library, for example, time.Time. The designers wanted to enforce that a time.Time struct is immutable. Hence, most methods such as After, IsZero, and UTC have a value receiver. But to comply with existing interfaces such as encoding.TextUnmarshaler, time.Time has to implement the UnmarshalBinary([]byte) error method, which mutates the receiver given a byte slice. Thus, this method has a pointer receiver. Consequently, mixing receiver types should be avoided in general but is not forbidden in 100% of cases.

Conclusion

We should now have a good understanding of whether to use value or pointer receivers. Of course, it’s impossible to be exhaustive, as there will always be edge cases, but this section’s goal was to provide guidance to cover most cases. By default, we can choose to go with a value receiver unless there’s a good reason not to do so. In doubt, we should use a pointer receiver.

References:

  • 100 Go Mistakes and how to avoid them, Teiva Harsanyi, Manning Publications Co

Previous Post
Next Post