8

As we all know, the difference between using a pointer or a value when defining a method in a Go struct is that when modifying an attribute value in a method, the modification made by a method defined by a value is limited to the method, while pointers do not have this limitation.

If the article ends here, then it will be quite ordinary, so I plan to take everyone to do a boring but worth thinking experiment.

Before starting, write a simple piece of code and run the things mentioned above. By the way, let everyone familiarize yourself with some of the coding rules of the next experimental code. Oh, by the way, the following code is written in 2021.08, and the Go version is 1.16.5 . If you see By the time of this article, Go has updated many versions, which may not be applicable. Not much nonsense, on the code:

package main

import "fmt"

type Foo struct {
    val int
}

/**
 *  在这里,我定义了两个 Set 方法,一个 P 结尾,一个 V 结尾,聪明的你肯定很快就反应过来了:
 *  P 即 Pointer,V 即 Value
 *
 * 另外我在这里加了个 callBy,方便追踪调用链
 */
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() {
    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("=====================================")
}

operation 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

As we expected, the modification of the attribute in the method defined by the value will not bring the impact to the outside.

Next, start our experiment

What would happen if the doll was used?

In our daily development, we often encounter another method being called in a method. What happens if the attribute is modified in the called method?

There are four situations for a doll: PV , VP , VV , PP ( may be more layers of dolls in the actual situation, but here we only need to understand one layer, and 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
}

Then add main()

func main() {
    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)
}

Results of the:

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

Make a table:

methodThe value f.val at the end of the main() callThe name of the first layer method / the value f.valThe second layer method name/the value f.val
SetPV()3SetPV(3) / 1SetV(3+1) / 4
SetVP()3SetVP(4) / 5SetP(4+1) / 5
SetVV()3SetVV(5) / 3SetV(5+1) / 6
SetPP()6SetPP(6) / 7SetP(6+1) / 7

It is concluded that only the method defined by pointers is used in the entire call link, and the modifications to the attributes will be retained, otherwise it will only be valid within the method and conform to the rules stated at the beginning.

At this point, you may think that the article is about to end, but not. Let’s focus on SetVP() :

func (f Foo) SetVP(v int, callBy string) {
    f.SetP(v+1, callBy+"->SetVP") // 看这里,这里可是指针喔,为什么它修改的值,也仅限于 SetVP() 内呢
    fmt.Printf("In SetVP(): call by:%s\tval:%d\n", callBy, f.val)
    f.val = v
}

Modify SetPP() which looks very similar:

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 /* 注释掉了这一行 */
}

After executing it, the modified value is not only inside SetPP() ! Does (f Foo) cause the internal (f *Foo) method to be copied?

Print out the pointer to confirm!

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)
}

// ... 省略其他方法的修改,都是一样的,只是换个名字而已

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() {
    f := Foo{0}
    fmt.Printf("In main():                      val:%d\n", f.val)
    // ... 省略其他没有修改的地方
}

Look at the results (I marked the three lines that need to be focused 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.val=0x14000124030
⚠️ In SetP():  &f=0x14000126030    &f.val=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:6

It can be found:

  1. Regardless of whether it is (f Foo) or (f *Foo) , inside the method, f itself is a copy
  2. Property address
    2.1. If it is a pointer method, the property address is inherited from the caller
    2.2. If it is a value method, the attribute address is the newly opened space address

As for whether more nesting doll calls will cause the memory to soar, I will not discuss it here. If you are interested, you can check the information yourself or look at the underlying implementation of Go itself.

Summarize

Before this article is finally over, let’s summarize what meaningful hints this boring experiment has for our actual development:

  • If you need to temporarily modify the properties of a method (for example, the current method needs to call other methods, and the target method will read the property value and you are not allowed to modify the target method), then you should define this method as value-passing
  • If you define a method to be passed by value, then remember that all the modifications you make directly or in the doll in this method will not be passed up (to the caller), but it will be passed down

This article was first published on my blog: https://yian.me/blog/what-is/go-struct-methods-on-values-or-pointers.html


Yian
1.2k 声望307 粉丝

土木狗,不会写代码