家喻户晓,Go struct 定义方法时应用指针还是值的区别就是在办法内批改属性值时,用值定义的办法所做的批改只限于办法内,而指针则没有这个局限。

文章如果到这里就完结了,那么就很平平无奇了,于是我打算带大家去做个无聊然而值得思考的试验。

在开始之前,先写段简略的代码跑一下后面说到的货色,顺便让大家相熟一下接下来试验代码的一些编码规定,哦对了,以下代码写于 2021.08,Go 版本是 1.16.5,如果你看到这篇文章的时候 Go 曾经更新了很多个版本了,可能就不实用了。废话不多说,上代码:

package mainimport "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("=====================================")}

运行后果:

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

如咱们预期,通过值定义的办法内对属性的批改并不会把影响带到内部。

接下来,开始咱们的试验

如果办法套娃,会产生什么?

在咱们日常开发时,常常会遇到办法里调用另一个办法,那如果被调用的办法里批改了属性,会产生什么呢?

套娃会有四种状况:PVVPVVPP理论状况可能还会呈现更多层的套娃,然而这里咱们只须要弄懂一层的,剩下能够依照数学归纳法去了解。),往代码中加四个 Set 办法:

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}

而后在 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)}

执行后果:

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

列个表格:

办法main() 调用完结时 f.val第一层办法名/第二层办法完结时 f.val第二层办法名/办法完结时 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

得出结论:只有整个调用链路都是用指针定义的办法,对属性做的批改才会保留,否则只会在办法内无效,合乎最开始说的规定。

到这里你可能认为文章就要完结了,然而并没有,咱们重点关注一下 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}

把长得很像的 SetPP() 批改一下:

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

执行它之后,它批改的值却不仅仅是在 SetPP() 外部!难道 (f Foo) 会导致外部的 (f *Foo) 办法也拷贝了一份?

把指针打印进去确认一下!

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)    // ... 省略其余没有批改的中央}

看看运行后果(我标记了须要重点关注的那三行):

In main():                      val:0⚠️ In main(): &f=0x14000124008     &f.val=0x14000124008====================================================In SetP():  &f=0x14000126020    &f.val=0x14000124008In SetP():  call by:main        val:1In main(): after f.SetP(1):     val:1====================================================In SetV():  &f=0x14000124010    &f.val=0x14000124010In SetV():  call by:main        val:2In main(): after f.SetV(2):     val:1====================================================In SetPV(): &f=0x14000126028    &f.val=0x14000124008In SetV():  &f=0x14000124018    &f.val=0x14000124018In SetV():  call by:main->SetPV val:4In SetPV(): call by:main        val:1In main(): after f.SetPV(3):    val:3====================================================⚠️ In SetVP(): &f=0x14000124030    &f.val=0x14000124030⚠️ In SetP():  &f=0x14000126030    &f.val=0x14000124030In SetP():  call by:main->SetVP val:5In SetVP(): call by:main        val:5In main(): after f.SetVP(4):    val:3====================================================In SetVV(): &f=0x14000124038    &f.val=0x14000124038In SetV():  &f=0x14000124060    &f.val=0x14000124060In SetV():  call by:main->SetVV val:6In SetVV(): call by:main        val:3In main(): after f.SetVV(5):    val:3====================================================In SetPP(): &f=0x14000126038    &f.val=0x14000124008In SetP():  &f=0x14000126040    &f.val=0x14000124008In SetP():  call by:main->SetPP val:7In SetPP(): call by:main        val:7In main(): after f.SetPP(6):    val:6

能够发现:

  1. 不论是 (f Foo) 还是 (f *Foo),在办法外部,f 自身都是拷贝的
  2. 属性地址
    2.1. 如果是指针办法,则属性地址继承于调用方
    2.2. 如果是值办法,则属性地址是新开拓的空间地址

至于说套娃调用多了会不会导致内存飙升,这里就不展开讨论了,有趣味的能够去本人查阅材料或者看看 Go 自身的底层实现。

总结

在这篇文章终于完结之前,总结一下这个无聊的试验对咱们的理论开发有哪些有意义的提醒:

  • 如果某个办法你须要对属性做长期批改(比方以后办法须要调用其余办法,而指标办法会读取属性值且你不被容许批改指标办法),那么你应该将这个办法定义为值传递的
  • 如果你某个办法定义为值传递的了,那么切记,你在这个办法内间接或者套娃所做的所有批改都不会不会向上传递(作用到调用者那里),然而它会向下传递

本文首发于自己博客:https://yian.me/blog/what-is/go-struct-methods-on-values-or-pointers.html