If you are a Gopher, you probably heard some advice that goes like this: when you edit a value then use a pointer receiver, when you read use a value receiver. That’s not always correct and you should be careful when using them.

Issue

Today I encountered an issue which can be simplified by the bellow code snippet:

 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
package main
 
import (
    "fmt"
    "time"
)
 
type Counter struct {
    Count int
}
 
// Method with pointer receiver
func (c *Counter) Increment() {
    go func() {
        for i := 0; i < 10; i++ {
            c.Count++
            time.Sleep(1 * time.Second)
        }
    }()
}
 
// Method with value receiver
func (c Counter) Display() {
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println("counter value: ", c.Count)
            time.Sleep(1 * time.Second)
        }
    }()
}
 
func main() {
    c := Counter{Count: 0}
    c.Display()
    c.Increment()
 
    time.Sleep(15 * time.Second)
    fmt.Println("counter final value: ", c.Count)
}

I have a struct with two methods, Increment uses the pointer receiver to change the property of the struct, and Display uses the value receiver to read the value from the struct. Here is the result:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
➜ go run .
counter value:  1
counter value:  1
counter value:  1
counter value:  1
counter value:  1
counter value:  1
counter value:  1
counter value:  1
counter value:  1
counter value:  1
counter final value:  11

Both methods concurrently edit and read the counter, but the method of Display can only show the counter’s initial value. Why does that happen?

It turns out that because of the value receiver of the method Display. When you call that method, it will create a copy of the Counter struct and operate on that copy. The Increment method changes the count on the original Struct, while the Display method reads from a copy of the Counter struct. Basically, two methods now are operating on two different struct instances.

To fix the issue, just use the pointer receiver in the Display method, and now both methods are operating on the same copy of the struct.

An another pattern is when you by mistake call an pointer receiver method inside an value receiver method, like this:

 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
package main

import (
	"fmt"
)

type Counter struct {
	mData map[int]int
	sData []int
}

// Method with pointer receiver
func (c *Counter) Put(i int) {
	c.mData[i] = i
	c.sData = append(c.sData, i)
	fmt.Println("Put mData=", c.mData)
	fmt.Println("Put sData=", c.sData)
}

// Method with value receiver
func (c Counter) Do() {
	// call api and get data
	i := 2
	c.Put(i)
}

func main() {
	c := Counter{mData: map[int]int{1: 1}, sData: []int{1}}
	c.Do()
	fmt.Println("main mData=", c.mData)
	fmt.Println("main sData=", c.sData)
}

The output:

1
2
3
4
Put mData= map[1:1 2:2]
Put sData= [1 2]
main mData= map[1:1 2:2]
main sData= [1]

This case is quite confusing when number 2 appears in the map, but disappear in the list 😂 The fix is similar to previous case, make the Do method become pointer receiver.

Sugar syntax

In Go, methods are just sugar syntax. To make things easier to understand, I rewrite the two methods as below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Function with pointer args
func Increment(c *Counter) {
    go func() {
        for i := 0; i < 10; i++ {
            c.Count++
            time.Sleep(1 * time.Second)
        }
    }()
}
 
// Function with value args
func Display(c Counter) {
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println("counter value: ", c.Count)
            time.Sleep(1 * time.Second)
        }
    }()
}

Method expression

Go also has something called Method Expressions, I can rewrite the main function as below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
    c := Counter{Count: 1}
 
    display := Counter.Display
    display(c)
    inc := (*Counter).Increment
    inc(&c)
 
    time.Sleep(15 * time.Second)
    fmt.Println("counter final value: ", c.Count)
}

From the code, you can see, that I can convert methods to functions, and pass the struct to the functions as the addition argument.

References

https://go.dev/ref/spec#Method_expressions https://golang.org/ref/spec#Method_values https://www.reddit.com/r/golang/comments/6p1woc/are_gos_methods_just_syntactic_sugar