Featured image of post Go's method receiver: Pointer vs Value

Go's method receiver: Pointer vs Value

The topic is large, and there is plenty of information online. This blog tries to keep it short and concise, and useful for experienced programmers that are new to Go.

Coming to Go from Python introduced me to a new concept I didn’t have to put thought into. Python is a pass-by-object-reference language. You have no direct control over that. What this means is, when you pass an object (everything in Python is an object) to a function, you pass a reference to the object itself.

We can use the id() function to illustrate that. It returns the identity of an object. This identity has to be unique and constant for this object during its lifetime.

1
2
3
4
5
6
7
8
9
>>> def action_with_string(s):
...     print(id(s))
...
>>> v = "sample string"
>>> id(v)
4360321520
>>> action_with_string(v)
4360321520
>>>

The object inside a function is as same as the one of the caller. Whatever you pass, the function can mutate (as long as it is mutable). It is discussed here on StackOverflow: Python functions call by reference if you are interested in reading further.

In Go, when you define a method on a struct, you have to choose the receiver type - value or a pointer receiver.

What does this means, anyway?

In simple terms, the value receiver makes a copy of the type and pass it to the function. The function stack now holds an equal object but at a different location on memory. If the method mutates the struct, it wouldn’t be reflected outside the function scope.

The pointer receiver passes the address of an object to the method. The function stack has a reference to the original object.

A simple example shows the difference. (Go Playground link)

 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
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
	"fmt"
)

type Bike struct {
	Model string
	Size  int
}

func ValueReceiverExample(b Bike) {
	fmt.Printf("VALUE :: The address of the received bike is: %p\n", &b)
	// Address of the object is different than the ones in main.
	// Changes to the object are scoped to the method, and not reflected to the caller.
	b.Model = "BMW"
	fmt.Println("INSIDE ValueReceiverExample :: model: ", b.Model)
}

func PointerReciverExample(b *Bike) {
	fmt.Printf("POINTER :: The address of the received bike is: %p\n", b)
	// Address of the object is as same as the ones in main.
	b.Model = "BMW"
	fmt.Println("INSIDE PointerReciverExample :: model: ", b.Model)
}

func main() {
	HondaBikeValue := Bike{"Honda CBR", 650}
	SuzukiBikePointer := &Bike{"Suzuki V-Storm", 650}

	fmt.Printf("Value object address in main: %p\n", &HondaBikeValue)
	fmt.Printf("Pointer object address in main: %p\n", SuzukiBikePointer)

	ValueReceiverExample(HondaBikeValue)
	PointerReciverExample(SuzukiBikePointer)

	fmt.Println("Value's model in main: ", HondaBikeValue.Model)
	fmt.Println("Pointer's model in main: ", SuzukiBikePointer.Model)
}

OUTPUT:
Value object address in main: 0xc00000c0a0
Pointer object address in main: 0xc00000c0c0
VALUE :: The address of the received bike is: 0xc00000c0e0
INSIDE ValueReceiverExample :: model:  BMW
POINTER :: The address of the received bike is: 0xc00000c0c0
INSIDE PointerReciverExample :: model:  BMW
Value's model in main:  Honda CBR
Pointer's model in main:  BMW

So when should you use what?

Regardless of what you choose, it is best practice to keep a uniformity of the struct methods. If the struct uses both types, it is hard to track which method use which receiver type, especially if you weren’t the one who wrote the code. Consistency is an important aspect of programming.

Pointers

  • If you want to share a value with its methods

    If the method modifies the state of the struct, you must use a pointer. Value receiver changes are local to the method scope.

  • If the struct is very large (optimization)

    Large structs with many fields may be costly to copy every time they need to be passed around. If you are in such a case, you should consider breaking it down into smaller pieces. If that’s not possible, use a pointer. Optimization usually adds complexity. Be aware it comes with a trade-off.

  • One major disadvantage of pointers is they are not safe for concurrency. If you use pointers, you need to use synchronous mechanisms such as channels or use the atomic / sync builtin packages.

Values

  • If you don’t want to share a value, use the value receiver.

  • Value receivers are concurrency safe. One of Go’s advantages is concurrency, so this is a huge plus. You never know when you would need it, and if you write library code you can be sure someone at some point will use it concurrently.

Summary

That was an interesting subject for me. It is a new concept I didn’t deal with before coding in Go. After researching the subject thru blogs, documentation, videos, and Stackoverflow, I have come up with a small set of rules to remember:

  1. Consistency counts. Use the same receiver type for all your methods. It isn’t always feasible, but have this rule in mind when you need to mix them.
  2. Method defines the behavior of a type; if the method updates or modifies the state, use the pointer receiver.
  3. If a method doesn’t mutate state, use-value receiver.
  4. Functions operate on values; functions should not depend on the state of a type.

Here’s a list of great resources if you want to explore deeper: