Should I use value receivers or pointer receivers?

Value receivers have some benefits include immutability, concurrent safety and clean logic (not always, often true). But to what extend can I use value receivers without an issue or performance penalty?

In the Go FAQ, there are 3 rules:

  1. most important, does the method need to modify the receiver? If it does, the receiver must be a pointer
  2. if the receiver is large, a big struct for instance, it will be much cheaper to use a pointer receiver
  3. if some of the methods of the type must have pointer receivers, the rest should too, so the method set is consistent regardless of how the type is used

Let’s look at rule 1. In many cases, an object needs to be modified by a method after its initialization, but it doesn’t mean the modification has to be done in place, an alternative and often a better way is to get a modified copy of the object, which is usually called a Fluent Interface. Basically it means a method with a value receiver and a return value of the same type. It’s just pure (no side effect) functional programming style under another name.

Then rule 2 tells us to choose based on the struct size, but how big is a big struct? We know that time.Time is 3-words large with value receivers, but how about 4-words, 5-words or bigger structs? To answer this question, I did some benchmarks.

The benchmarks are based on the idea that a type with value receivers would need to implement fluent interface, so it should compare the performance between two fluent methods with value and pointer receivers:

func (s S) ByValue() S {
    // ...
	return s
}

func (s *S) ByPointer() *S {
    // ...
	return s
}

It turned out inlining is critical to the overhead of value copying.

When inlining is disabled (gcflags=-l), for a struct of 1 word, value receiver has the same performance as the pointer receiver, but for structs larger than 2 words, value receivers begin to show more and more overhead.

Without Inline

However, when inlining is enabled (the default option), value receivers have almost the same performance as pointer receivers for struct size from 1 up to 9 words! Overhead of value copying starts to rise for structs of 10 words or more.

With Inline

These results were tested on a Macbook Pro with go1.14.3.

Conclusion

From the benchmark results, value receivers should be preferred if the methods are inlined and the struct size is equal to or smaller than 9 words (1 word == 64 bit), but this might not be the end of the story, the more complex a method is, the less likely it is inlined, but also the less proportion the copying overhead to the whole method logic. The overhead of copying from a value receiver might not be significant to the overall running time, so at the end of the day, the choice depends on the frequency of the method calls, the business logic and the performance requirements. When in doubt, benchmark it!