木鸟杂记

大规模数据系统

'Go Notes (I): Value Methods vs Pointer Methods'

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:

  1. Value methods can be called on both pointers and values, but pointer methods can only be called on pointers.
  2. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"fmt"
)

type Foo struct {
name string
}

func (f *Foo) PointerMethod() {
fmt.Println("pointer method on", f.name)
}

func (f Foo) ValueMethod() {
fmt.Println("value method on", f.name)
}

func NewFoo() Foo { // returns an rvalue
return Foo{name: "right value struct"}
}


func main() {
f1 := Foo{name: "value struct"}
f1.PointerMethod() // The compiler automatically inserts the address operator, becoming (&f1).PointerMethod()
f1.ValueMethod()

f2 := &Foo{name: "pointer struct"}
f2.PointerMethod()
f2.ValueMethod() // The compiler automatically dereferences, becoming (*f2).PointerMethod()

NewFoo().ValueMethod()
NewFoo().PointerMethod() // Error!!!
}

The last line produces the following error:

1
2
./pointer_method.go:34:10: cannot call pointer method on NewFoo()
./pointer_method.go:34:10: cannot take the address of 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
2
3
4
5
6
7
func (f *Foo) PointerMethod() {
fmt.Println("pointer method on ", f.name)
}

func (f Foo) ValueMethod() {
fmt.Println("value method on", f.name)
}

This can be converted into the following two functions for consideration:

1
2
3
4
5
6
7
func PointerMethod(f *Foo) {
fmt.Println("pointer method on ", f.name)
}

func ValueMethod(f Foo) {
fmt.Println("value method on", f.name)
}

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:

  1. Does the method need to modify the receiver itself? If so, the receiver must be a pointer.
  2. Efficiency. If the receiver is a value, the method call will inevitably cause a struct copy, and copying large objects is expensive.
  3. 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

  1. effective go: https://golang.org/doc/effective_go.html#pointers_vs_values
  2. golang faq: https://golang.org/doc/faq#methods_on_values_or_pointers
  3. golang code review comments: https://github.com/golang/go/wiki/CodeReviewComments#receiver-type
  4. stackoverflow: https://stackoverflow.com/questions/27775376/value-receiver-vs-pointer-receiver

我是青藤木鸟,一个喜欢摄影、专注大规模数据系统的程序员,欢迎关注我的公众号:“木鸟杂记”,有更多的分布式系统、存储和数据库相关的文章,欢迎关注。 关注公众号后,回复“资料”可以获取我总结一份分布式数据库学习资料。 回复“优惠券”可以获取我的大规模数据系统付费专栏《系统日知录》的八折优惠券。

我们还有相关的分布式系统和数据库的群,可以添加我的微信号:qtmuniao,我拉你入群。加我时记得备注:“分布式系统群”。 另外,如果你不想加群,还有一个分布式系统和数据库的论坛(点这里),欢迎来玩耍。

wx-distributed-system-s.jpg