As we all know, the difference between a pointer and a value when defining a Go struct method is that when modifying a property value within a method, the value defined method changes only within the method, while the pointer does not have this limitation.

This article would be boring if it ended there, so I’m going to take you to a boring but thought-provoking experiment.

Before we get started, let’s write some simple code to run through what we’ve already mentioned and familiarize ourselves with some of the coding rules for the upcoming experiment code. By the way, the following code was written in 2021.08 and the Go version is 1.16.5. By the time you read this article, Go has been updated many times and may not be applicable. No more nonsense, on the code:

package main

import "fmt"

type Foo struct {
	val int
}

/** * I've defined two Set methods: * P = Pointer and V = Value ** I've added a callBy to trace the call chain */
func (f *Foo) SetP(v int, callBy string) {
	f.val = v
	fmt.Printf("In SetP(): call by:%s\tval:%d\n", callBy, f.val)
}

func (f Foo) SetV(v int, callBy string) {
	f.val = v
	fmt.Printf("In SetV(): call by:%s\tval:%d\n", callBy, f.val)
}


func main(a) {
	f := Foo{0}
	fmt.Printf("In main(): val:%d\n", f.val)
	fmt.Println("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =")

	f.SetP(1."main")
	fmt.Printf("In main(): after f.SetP(1): val:%d\n", f.val)
	fmt.Println("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =")

	f.SetV(2."main")
	fmt.Printf("In main(): after f.SetV(2): val:%d\n", f.val)
	fmt.Println("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =")}Copy the code

Running results:

In main():                      val:0
=====================================
In SetP():  call by:main        val:1
In main(): after f.SetP(1):     val:1
=====================================
In SetV():  call by:main        val:2
In main(): after f.SetV(2):     val:1
Copy the code

As expected, changes to attributes within methods defined by values do not carry impact externally.

Next, let’s start our experiment

What happens if the method is a doll?

In our daily development, we often encounter a method that calls another method. What happens if a property is changed in the called method?

Dolls will have four cases: PV, VP, VV, PP (the actual situation may also appear more layers of dolls, but here we only need to understand one layer, the rest can be understood by mathematical induction.) Add four Set methods to the code:

func (f *Foo) SetPV(v int, callBy string) {
	f.SetV(v+1, callBy+"->SetPV")
	fmt.Printf("In SetPV(): call by:%s\tval:%d\n", callBy, f.val)
	f.val = v
}

func (f Foo) SetVP(v int, callBy string) {
	f.SetP(v+1, callBy+"->SetVP")
	fmt.Printf("In SetVP(): call by:%s\tval:%d\n", callBy, f.val)
	f.val = v
}

func (f *Foo) SetPP(v int, callBy string) {
	f.SetP(v+1, callBy+"->SetPP")
	fmt.Printf("In SetPP(): call by:%s\tval:%d\n", callBy, f.val)
	f.val = v
}

func (f Foo) SetVV(v int, callBy string) {
	f.SetV(v+1, callBy+"->SetVV")
	fmt.Printf("In SetVV(): call by:%s\tval:%d\n", callBy, f.val)
	f.val = v
}
Copy the code

Then add to main() :

func main(a) {
	f := Foo{0}
	fmt.Printf("In main(): val:%d\n", f.val)
	fmt.Println("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =")

	f.SetP(1."main")
	fmt.Printf("In main(): after f.SetP(1): val:%d\n", f.val)
	fmt.Println("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =")

	f.SetV(2."main")
	fmt.Printf("In main(): after f.SetV(2): val:%d\n", f.val)
	fmt.Println("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =")

	f.SetPV(3."main")
	fmt.Printf("In main(): after f.SetPV(3): val:%d\n", f.val)
	fmt.Println("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =")

	f.SetVP(4."main")
	fmt.Printf("In main(): after f.SetVP(4): val:%d\n", f.val)
	fmt.Println("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =")

	f.SetVV(5."main")
	fmt.Printf("In main(): after f.SetVV(5): val:%d\n", f.val)
	fmt.Println("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =")

	f.SetPP(6."main")
	fmt.Printf("In main(): after f.SetPP(6): val:%d\n", f.val)
}
Copy the code

Execution Result:

In main():                      val:0
=====================================
In SetP():  call by:main        val:1
In main(): after f.SetP(1):     val:1
=====================================
In SetV():  call by:main        val:2
In main(): after f.SetV(2):     val:1
=====================================
In SetV():  call by:main->SetPV val:4
In SetPV(): call by:main        val:1
In main(): after f.SetPV(3):    val:3
=====================================
In SetP():  call by:main->SetVP val:5
In SetVP(): call by:main        val:5
In main(): after f.SetVP(4):    val:3
=====================================
In SetV():  call by:main->SetVV val:6
In SetVV(): call by:main        val:3
In main(): after f.SetVV(5):    val:3
=====================================
In SetP():  call by:main->SetPP val:7
In SetPP(): call by:main        val:7
In main(): after f.SetPP(6):    val:6
Copy the code

Make a table:

methods When the main() call endsf.val Layer 1 method name/End of layer 2 methodf.val Layer 2 method name/method endf.val
SetPV() 3 SetPV(3) / 1 SetV(3+1) / 4
SetVP() 3 SetVP(4) / 5 SetP(4+1) / 5
SetVV() 3 SetVV(5) / 3 SetV(5+1) / 6
SetPP() 6 SetPP(6) / 7 SetP(6+1) / 7

We conclude that changes to attributes are retained only if the entire invocation link is a pointer defined method, otherwise they will only be valid within the method, in accordance with the original rules.

At this point you might think the article is over, but it isn’t. Let’s focus on SetVP() :

func (f Foo) SetVP(v int, callBy string) {
	f.SetP(v+1, callBy+"->SetVP") SetVP() = SetVP()
	fmt.Printf("In SetVP(): call by:%s\tval:%d\n", callBy, f.val)
	f.val = v
}
Copy the code

Modify SetPP(), which looks similar:

func (f *Foo) SetPP(v int, callBy string) {
	f.SetP(v+1, callBy+"->SetPP") // This is also a pointer
	fmt.Printf("In SetPP(): call by:%s\tval:%d\n", callBy, f.val)
	// f.val = v /* comment out this line */
}
Copy the code

After executing it, it modifies values not just inside SetPP()! Does (f Foo) cause a copy of the internal (f *Foo) method?

Print out the pointer to be sure!

func (f *Foo) SetP(v int, callBy string) {
	fmt.Printf("In SetP(): &f=%p\t&f.val=%p\n", &f, &f.val)
	f.val = v
	fmt.Printf("In SetP(): call by:%s\tval:%d\n", callBy, f.val)
}

/ /... If you omit any other method, it's the same thing, just change the name

func (f Foo) SetVP(v int, callBy string) {
	fmt.Printf("In SetVP(): &f=%p\t&f.val=%p\n", &f, &f.val)
	f.SetP(v+1, callBy+"->SetVP")
	fmt.Printf("In SetVP(): call by:%s\tval:%d\n", callBy, f.val)
	f.val = v
}

func main(a) {
	f := Foo{0}
	fmt.Printf("In main(): val:%d\n", f.val)
    / /... Leave out other unmodified areas
}
Copy the code

Take a look at the result (I’ve highlighted the three lines to focus on) :

In main(): val:0 ⚠️ In main(): &f=0x14000124008 &f.val=0x14000124008 ==================================================== In SetP(): &f=0x14000126020 &f.val=0x14000124008 In SetP(): call by:main val:1 In main(): after f.SetP(1): val:1 ==================================================== In SetV(): &f=0x14000124010 &f.val=0x14000124010 In SetV(): call by:main val:2 In main(): after f.SetV(2): val:1 ==================================================== In SetPV(): &f=0x14000126028 &f.val=0x14000124008 In SetV(): &f=0x14000124018 &f.val=0x14000124018 In SetV(): call by:main->SetPV val:4 In SetPV(): call by:main val:1 In main(): after f.SetPV(3): Val: 3 = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = ⚠ ️ In SetVP () : &f= 0x14000124030&f al=0x14000124030 ⚠️ In SetP(): &f= 0x14000126030&f al=0x14000124030 In SetP(): call by:main->SetVP val:5 In SetVP(): call by:main val:5 In main(): after f.SetVP(4): val:3 ==================================================== In SetVV(): &f=0x14000124038 &f.val=0x14000124038 In SetV(): &f=0x14000124060 &f.val=0x14000124060 In SetV(): call by:main->SetVV val:6 In SetVV(): call by:main val:3 In main(): after f.SetVV(5): val:3 ==================================================== In SetPP(): &f=0x14000126038 &f.val=0x14000124008 In SetP(): &f=0x14000126040 &f.val=0x14000124008 In SetP(): call by:main->SetPP val:7 In SetPP(): call by:main val:7 In main(): after f.SetPP(6): val:6Copy the code

It can be found:

  1. Whether it’s(f Foo)or(f *Foo), within the method,fIt’s all copied itself
  2. Property address

2.1. In the case of a pointer method, the property address inherits from the caller 2.2. In the case of a value method, the property address is the newly created spatial address

As to whether the doll call will lead to memory surge, there is no discussion here, interested can Go to their own information or look at the underlying implementation of Go.

conclusion

Before this article finally comes to an end, let’s summarize what meaningful hints this boring experiment has for our actual development:

  • If you need to make temporary changes to a property of a method (for example, if the current method needs to call another method, and the target method reads the property value and you are not allowed to change the target method), then you should define the method as value passed
  • If you have a method defined as value passed, remember that any changes you make directly or to the doll in the method will not be passed up (to the caller), but it will be passed down

This article was first published on my blog: yian.me/blog/what-i…