首页>>后端>>Golang->一文彻底搞懂Go结构体方法指针和值的区别

一文彻底搞懂Go结构体方法指针和值的区别

时间:2023-11-30 本站 点击:0

众所周知,Go struct 定义方法时使用指针还是值的区别就是在方法内修改属性值时,用值定义的方法所做的修改只限于方法内,而指针则没有这个局限。

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

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

packagemainimport"fmt"typeFoostruct{valint}/***在这里,我定义了两个Set方法,一个P结尾,一个V结尾,聪明的你肯定很快就反应过来了:*P即Pointer,V即Value**另外我在这里加了个callBy,方便追踪调用链*/func(f*Foo)SetP(vint,callBystring){f.val=vfmt.Printf("InSetP():callby:%s\tval:%d\n",callBy,f.val)}func(fFoo)SetV(vint,callBystring){f.val=vfmt.Printf("InSetV():callby:%s\tval:%d\n",callBy,f.val)}funcmain(){f:=Foo{0}fmt.Printf("Inmain():val:%d\n",f.val)fmt.Println("=====================================")f.SetP(1,"main")fmt.Printf("Inmain():afterf.SetP(1):val:%d\n",f.val)fmt.Println("=====================================")f.SetV(2,"main")fmt.Printf("Inmain():afterf.SetV(2):val:%d\n",f.val)fmt.Println("=====================================")}

运行结果:

Inmain():val:0=====================================InSetP():callby:mainval:1Inmain():afterf.SetP(1):val:1=====================================InSetV():callby:mainval:2Inmain():afterf.SetV(2):val:1

如我们预期,通过值定义的方法内对属性的修改并不会把影响带到外部。

接下来,开始我们的实验

假如方法套娃,会发生什么?

在我们日常开发时,经常会遇到方法里调用另一个方法,那假如被调用的方法里修改了属性,会发生什么呢?

套娃会有四种情况:PVVPVVPP实际情况可能还会出现更多层的套娃,但是这里我们只需要弄懂一层的,剩下可以按照数学归纳法去理解。),往代码中加四个 Set 方法:

func(f*Foo)SetPV(vint,callBystring){f.SetV(v+1,callBy+"->SetPV")fmt.Printf("InSetPV():callby:%s\tval:%d\n",callBy,f.val)f.val=v}func(fFoo)SetVP(vint,callBystring){f.SetP(v+1,callBy+"->SetVP")fmt.Printf("InSetVP():callby:%s\tval:%d\n",callBy,f.val)f.val=v}func(f*Foo)SetPP(vint,callBystring){f.SetP(v+1,callBy+"->SetPP")fmt.Printf("InSetPP():callby:%s\tval:%d\n",callBy,f.val)f.val=v}func(fFoo)SetVV(vint,callBystring){f.SetV(v+1,callBy+"->SetVV")fmt.Printf("InSetVV():callby:%s\tval:%d\n",callBy,f.val)f.val=v}

然后在 main() 里加上:

funcmain(){f:=Foo{0}fmt.Printf("Inmain():val:%d\n",f.val)fmt.Println("=====================================")f.SetP(1,"main")fmt.Printf("Inmain():afterf.SetP(1):val:%d\n",f.val)fmt.Println("=====================================")f.SetV(2,"main")fmt.Printf("Inmain():afterf.SetV(2):val:%d\n",f.val)fmt.Println("=====================================")f.SetPV(3,"main")fmt.Printf("Inmain():afterf.SetPV(3):val:%d\n",f.val)fmt.Println("=====================================")f.SetVP(4,"main")fmt.Printf("Inmain():afterf.SetVP(4):val:%d\n",f.val)fmt.Println("=====================================")f.SetVV(5,"main")fmt.Printf("Inmain():afterf.SetVV(5):val:%d\n",f.val)fmt.Println("=====================================")f.SetPP(6,"main")fmt.Printf("Inmain():afterf.SetPP(6):val:%d\n",f.val)}

执行结果:

Inmain():val:0=====================================InSetP():callby:mainval:1Inmain():afterf.SetP(1):val:1=====================================InSetV():callby:mainval:2Inmain():afterf.SetV(2):val:1=====================================InSetV():callby:main->SetPVval:4InSetPV():callby:mainval:1Inmain():afterf.SetPV(3):val:3=====================================InSetP():callby:main->SetVPval:5InSetVP():callby:mainval:5Inmain():afterf.SetVP(4):val:3=====================================InSetV():callby:main->SetVVval:6InSetVV():callby:mainval:3Inmain():afterf.SetVV(5):val:3=====================================InSetP():callby:main->SetPPval:7InSetPP():callby:mainval:7Inmain():afterf.SetPP(6):val:6

列个表格: |方法|main() 调用结束时 f.val 值 | 第一层方法名/第二层方法结束时 f.val 值|第二层方法名/方法结束时 f.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|

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

到这里你可能以为文章就要结束了,但是并没有,我们重点关注一下 SetVP()

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

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

func(f*Foo)SetPP(vint,callBystring){f.SetP(v+1,callBy+"->SetPP")//这里也是指针fmt.Printf("InSetPP():callby:%s\tval:%d\n",callBy,f.val)//f.val=v/*注释掉了这一行*/}

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

把指针打印出来确认一下!

func(f*Foo)SetP(vint,callBystring){fmt.Printf("InSetP():&f=%p\t&f.val=%p\n",&f,&f.val)f.val=vfmt.Printf("InSetP():callby:%s\tval:%d\n",callBy,f.val)}//...省略其他方法的修改,都是一样的,只是换个名字而已func(fFoo)SetVP(vint,callBystring){fmt.Printf("InSetVP():&f=%p\t&f.val=%p\n",&f,&f.val)f.SetP(v+1,callBy+"->SetVP")fmt.Printf("InSetVP():callby:%s\tval:%d\n",callBy,f.val)f.val=v}funcmain(){f:=Foo{0}fmt.Printf("Inmain():val:%d\n",f.val)//...省略其他没有修改的地方}

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

Inmain():val:0⚠️Inmain():&f=0x14000124008&f.val=0x14000124008====================================================InSetP():&f=0x14000126020&f.val=0x14000124008InSetP():callby:mainval:1Inmain():afterf.SetP(1):val:1====================================================InSetV():&f=0x14000124010&f.val=0x14000124010InSetV():callby:mainval:2Inmain():afterf.SetV(2):val:1====================================================InSetPV():&f=0x14000126028&f.val=0x14000124008InSetV():&f=0x14000124018&f.val=0x14000124018InSetV():callby:main->SetPVval:4InSetPV():callby:mainval:1Inmain():afterf.SetPV(3):val:3====================================================⚠️InSetVP():&f=0x14000124030&f.val=0x14000124030⚠️InSetP():&f=0x14000126030&f.val=0x14000124030InSetP():callby:main->SetVPval:5InSetVP():callby:mainval:5Inmain():afterf.SetVP(4):val:3====================================================InSetVV():&f=0x14000124038&f.val=0x14000124038InSetV():&f=0x14000124060&f.val=0x14000124060InSetV():callby:main->SetVVval:6InSetVV():callby:mainval:3Inmain():afterf.SetVV(5):val:3====================================================InSetPP():&f=0x14000126038&f.val=0x14000124008InSetP():&f=0x14000126040&f.val=0x14000124008InSetP():callby:main->SetPPval:7InSetPP():callby:mainval:7Inmain():afterf.SetPP(6):val:6

可以发现:

不管是 (f Foo) 还是 (f *Foo),在方法内部,f 本身都是拷贝的

属性地址 2.1. 如果是指针方法,则属性地址继承于调用方 2.2. 如果是值方法,则属性地址是新开辟的空间地址

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

总结

在这篇文章终于结束之前,总结一下这个无聊的实验对我们的实际开发有哪些有意义的提示:

如果某个方法你需要对属性做临时修改(比如当前方法需要调用其他方法,而目标方法会读取属性值且你不被允许修改目标方法),那么你应该将这个方法定义为值传递的

如果你某个方法定义为值传递的了,那么切记,你在这个方法内直接或者套娃所做的一切修改都不会不会向上传递(作用到调用者那里),但是它会向下传递

作者:Yian


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/Golang/4557.html