Introduction
Recently, while writing Go code, I needed to customize a string conversion method for a struct:
1 | func (ms MyStruct) String() string |
However, I got stuck when deciding whether to use a value method or a pointer method for the implementation.
Go’s syntactic sugar makes these two approaches consistent at the call site, which made it difficult for me to decide which was better. So I decided to dig deeper into the underlying principles so that I could write more idiomatic Go code in the future.
Author: 木鸟杂记 https://www.qtmuniao.com, please indicate the source when reprinting
Differences
In the official effective go documentation, the difference between the two is precisely described:
The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers.
There is a handy exception, though. When the value is addressable, the language takes care of the common case of invoking a pointer method on a value by inserting the address operator automatically.
The gist is as follows:
- Value methods can be called on both pointers and values, but pointer methods can only be called on pointers.
- However, there is an exception: if a value is addressable (an lvalue), the compiler will automatically insert the address operator when a value calls a pointer method, making it appear as though pointer methods can also be called on values in this case.
After reading this explanation, I have a few words I probably shouldn’t say. Go’s syntactic sugar feels great at first, but the more you use it, the more you realize it introduces a lot of semantic complexity, placing a heavy burden on your mental model. For example: type assertions, embedding, automatic dereferencing, automatic address operator insertion, automatic semicolon insertion, and so on—I won’t complain about each one. It all comes down to trade-offs; you can’t have your cake and eat it too.
Enough talk—let’s get straight to a small example:
1 | package main |
The last line produces the following error:
1 | ./pointer_method.go:34:10: cannot call pointer method on NewFoo() |
It seems the compiler first tries to call the pointer method on the rvalue returned by NewFoo(), which fails; then it tries to insert the address operator, which also fails, so it has no choice but to report an error.
As for the difference between lvalues and rvalues, feel free to look it up if you’re interested. Roughly speaking, the most important distinction is whether it can be addressed: values that can be addressed are lvalues, which can appear on either side of an assignment; values that cannot be addressed are rvalues, such as function return values, literals, constants, etc., which can only appear on the right side of an assignment.
Trade-offs
For a specific scenario, deciding between the two is actually equivalent to another question: when defining a function, should you pass by value or by pointer?
For example, in the case above:
1 | func (f *Foo) PointerMethod() { |
This can be converted into the following two functions for consideration:
1 | func PointerMethod(f *Foo) { |
In Go terminology, think of the function’s receiver as an argument.
So, pass by value or by pointer? This is almost a soul-searching question encountered in every language. Of course, Java is the first to disagree, but I won’t expand on that here—feel free to Google it if interested.
When deciding whether a receiver should be a value or a pointer, the main considerations are:
- Does the method need to modify the receiver itself? If so, the receiver must be a pointer.
- Efficiency. If the receiver is a value, the method call will inevitably cause a struct copy, and copying large objects is expensive.
- Consistency. For methods on the same struct, mixing value methods and pointer methods is definitely not elegant.
So when should you use value methods? For simple immutable objects, using value methods can reduce the GC burden—that seems to be about the only benefit. So please remember:
When in doubt, use a pointer method.
Of course, the Go experts were worried you might still have questions, so they detailed some common cases for you—see here: https://github.com/golang/go/wiki/CodeReviewComments#receiver-type
References
- effective go: https://golang.org/doc/effective_go.html#pointers_vs_values
- golang faq: https://golang.org/doc/faq#methods_on_values_or_pointers
- golang code review comments: https://github.com/golang/go/wiki/CodeReviewComments#receiver-type
- stackoverflow: https://stackoverflow.com/questions/27775376/value-receiver-vs-pointer-receiver
