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

作者&投稿:广裘 (若有异议请与网页底部的电邮联系)
~

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

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

在开始之前,先写段简单的代码跑一下前面说到的东西,顺便让大家熟悉一下接下来实验代码的一些编码规则,哦对了,以下代码写于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:%sval:%d
",callBy,f.val)}func(fFoo)SetV(vint,callBystring){f.val=vfmt.Printf("InSetV():callby:%sval:%d
",callBy,f.val)}funcmain(){f:=Foo{0}fmt.Printf("Inmain():val:%d
",f.val)fmt.Println("=====================================")f.SetP(1,"main")fmt.Printf("Inmain():afterf.SetP(1):val:%d
",f.val)fmt.Println("=====================================")f.SetV(2,"main")fmt.Printf("Inmain():afterf.SetV(2):val:%d
",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

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

接下来,开始我们的实验

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

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

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

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

然后在main()里加上:

funcmain(){f:=Foo{0}fmt.Printf("Inmain():val:%d
",f.val)fmt.Println("=====================================")f.SetP(1,"main")fmt.Printf("Inmain():afterf.SetP(1):val:%d
",f.val)fmt.Println("=====================================")f.SetV(2,"main")fmt.Printf("Inmain():afterf.SetV(2):val:%d
",f.val)fmt.Println("=====================================")f.SetPV(3,"main")fmt.Printf("Inmain():afterf.SetPV(3):val:%d
",f.val)fmt.Println("=====================================")f.SetVP(4,"main")fmt.Printf("Inmain():afterf.SetVP(4):val:%d
",f.val)fmt.Println("=====================================")f.SetVV(5,"main")fmt.Printf("Inmain():afterf.SetVV(5):val:%d
",f.val)fmt.Println("=====================================")f.SetPP(6,"main")fmt.Printf("Inmain():afterf.SetPP(6):val:%d
",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:%sval:%d
",callBy,f.val)f.val=v}

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

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

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

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

func(f*Foo)SetP(vint,callBystring){fmt.Printf("InSetP():&f=%p&f.val=%p
",&f,&f.val)f.val=vfmt.Printf("InSetP():callby:%sval:%d
",callBy,f.val)}//...省略其他方法的修改,都是一样的,只是换个名字而已func(fFoo)SetVP(vint,callBystring){fmt.Printf("InSetVP():&f=%p&f.val=%p
",&f,&f.val)f.SetP(v+1,callBy+"->SetVP")fmt.Printf("InSetVP():callby:%sval:%d
",callBy,f.val)f.val=v}funcmain(){f:=Foo{0}fmt.Printf("Inmain():val:%d
",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

可以发现:

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

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

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

总结

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

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

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

作者:Yian




离中考还有30多天,我的英语一塌糊涂,想来个速成,不怕苦!
fetch\/get:去把东西带来(go and bring) carry:携带,搬运(无方向性)●some和any都可以修饰可数名词复数和不可数名词。区别如下: some用于肯定句中,如果用于疑问句,则表示希望得到肯定的回答或表示诚意。 any用于否定句、疑问句和条件状语从句,如果用在肯定句则表示“任何一个”的意思。5. That sounds good.(P27...

英语学习计划
详情请查看视频回答

英语阅读理解怎么提高啊?
2、语法结构:英语和汉语的语法结构不一样,词汇的搭配方式,表达方式也与汉语又很大的出入,所以基本的语法结构要掌握,这样才能做到句子通顺,无语法错误。 3、...4、求质不求量,把听写的文章彻底搞懂足矣,不要好大喜功,贪大贪快。扎扎实实,按部就班,是学好英语的必经之路。 5、把零碎的时间充分利用起来学英语...

怎么样阅读英文?
2、语法结构:英语和汉语的语法结构不一样,词汇的搭配方式,表达方式也与汉语又很大的出入,所以基本的语法结构要掌握,这样才能做到句子通顺,无语法错误。 3、...4、求质不求量,把听写的文章彻底搞懂足矣,不要好大喜功,贪大贪快。扎扎实实,按部就班,是学好英语的必经之路。 5、把零碎的时间充分利用起来学英语...

如何学习英语
2、语法结构:英语和汉语的语法结构不一样,词汇的搭配方式,表达方式也与汉语又很大的出入,所以基本的语法结构要掌握,这样才能做到句子通顺,无语法错误。 3、...4、求质不求量,把听写的文章彻底搞懂足矣,不要好大喜功,贪大贪快。扎扎实实,按部就班,是学好英语的必经之路。 5、把零碎的时间充分利用起来学英语...

怎样学好英语作文 怎样学好英语英语作文带翻译
2、语法结构:英语和汉语的语法结构不一样,词汇的搭配方式,表达方式也与汉语又很大的出入,所以基本的语法结构要掌握,这样才能做到句子通顺,无语法错误。 3、...4、求质不求量,把听写的文章彻底搞懂足矣,不要好大喜功,贪大贪快。扎扎实实,按部就班,是学好英语的必经之路。 5、把零碎的时间充分利用起来学英语...

英语高手进
2)含go的短语有:go swimming去游泳, goes on继续;持续, go to school去上学, go to bed上床睡觉...的短语连用,若要连用,就必须改变结构,可变为:I have had this dictionary for three years.因此在...两个与语法、词汇的关联比较大一些,平时一定要多做题,并且同种语法现象的题目要归类总结,争取完全搞懂...

求高人告诉怎么学好英语
2)含go的短语有:go swimming去游泳, goes on继续;持续, go to school去上学, go to bed上床睡觉...的短语连用,若要连用,就必须改变结构,可变为:I have had this dictionary for three years.因此在...学生是不可能读好英语或写好英语的,也就是说要先学习「听、讲」->「语」然后在学「文」,也就是...

蒙阴县17690355305: 结构体中的指针是什么意思呢? -
汝迹济尼: 1、结构体中的指针和结构体中的其他变量没有什么不同,结构体中的一个成员变量而已.只是这个变量是指针类型,和其他普通指针一样使用. 2、看变量名和结构体名称,你发的这种应该一般用在链表的实现中,这个Node结构体就是链表中的一个节点,然后结构体中定义一个指针变量next,用于指向下一个节点.

蒙阴县17690355305: go语言 结构体作为返回值 传的是指针吗 -
汝迹济尼: 这个是根据你值的内容来定的啊,看代码123456789101112131415 type User struct{ Name string }//例1(返回指针) func test1()*User{ returnnew(User) }//例2(返回指针) func test2()*User{ return&User{} }//例3(返回值) func test3()User{ returnUser{} } 明白没有?

蒙阴县17690355305: 有没有关于结构体和指针的详解...求大神
汝迹济尼: 我来告诉你标准答案!结构体你就将它理解为一个你自己创建的数据类型,可以自己封装数据; 指针你就当它是地址,当然需要你关注的是指针的类型,指针的类型也就是说你这个地址n能够访问多大的空间,int类型的指针一般4个字节; 给你举一个例子 int*p=某一个地址;对应的可以访问4个字节 你将(char*)p; 你就可以这样*p访问4个字节中的一个字节;

蒙阴县17690355305: 关于结构体的指针
汝迹济尼: 具体指向未定.但是用来指向tagNode的变量的. 使用new或malloc开辟的空间. 这是为制作链表定义的结构体.在程序中,我们可以使用new开辟新的节点.例如: pNode=new (tagNode); 这样pNode就指向一个节点,你可以执行pNode->pltem="jkdhkdh"; pNode->pNext=null; 一般情况下,还会有一个专门产生节点的指针,比如pNew就是tagNode的指针,那么可以 pNew=new(tagNode); 然后填写数据最后将节点连接到链表中 pNode->pNext=pNew 当然这只是一两个节点,若结点过多,还必须有一个指向要加入节点的指针.

蒙阴县17690355305: 结构体变量各个成员的表示方法 (*结构体指针变量).成员名 结构体指针变量 - >成员名 请问括号 -
汝迹济尼: 如果已经定义了结构体,取名test,并且假设定义了结构体指针,取名*ptr typedef struct {int a; }STR; //定义结构体 STR test; // 定义结构体变量 STR *ptr;//定义结构体指针 ptr=&test;// 指针指向结构体变量那么引用结构体里的成员a时 test.a (*ptr).a ptr->a 这三者是等价的

蒙阴县17690355305: 怎样在函数中为结构体指针数组赋值? -
汝迹济尼: 在函数中给结构体指针数组赋值,分两种情况了,第一,若你已经有结构体对象,只是要将指针指向已经分配内存的结构体,可以用取地址的&来给指针赋值;第二,若你只定义了结构体指针,可用malloc给指针初始化,如:已经有struct student结构体,定义student *p;给p初始化则用 p=(student*)malloc(sizeof(student)).

蒙阴县17690355305: C语言的结构体及指针 -
汝迹济尼: int 是一种 数据类型 结构体 是一种 数据类型 int 是C语言预先定义好的 数据类型 结构体 是用现有的数据类型组合起来的 数据类型 我需要一个变量来存储整数, C语言已经有int这样的数据类型,我就直接声明一个int型变量:int a; 再把数据存进...

蒙阴县17690355305: 定义一个结构体指针变量 -
汝迹济尼: 1、结构体指针的定义: struct node {int p;char node;char addr[30]; }a , *b; 2、结构体指针的使用: b=&a; a.p与(*b).p 与p->num是相同的意思. a.node与(*b).node与p->node是相同的意思. a.addr与(*b).addr与p->addr是相同的意思. 3、注意事项: (1)、结构体指针必须指向一个确定的结构体变量,如:p=&a; (2)、通过结构体变量的指针访问结构体的方法为: a.num、(*p).num、p->num.

蒙阴县17690355305: 结构体后面的指针怎么用的 -
汝迹济尼: 这句的意思就是定义两个该结构体类型的指针,linklist就是结构体指针类型,你可以用head->p,或者last->p来访问结构体的成员.

蒙阴县17690355305: 结构体指针与结构体指针的指针的区别? -
汝迹济尼: 假设有以下代码: ListNodePtr s; *ListNodePtr p=&s; **ListNodePtr pp=&p; 假设ListNodePtr s在内存中存放的地址为1000,p指向s,pp指向p,示意如下: 1000:s的内容 1005:p的内容=1000 1009:pp的内容=1005 即s的地址是1000,代表结构体的...

本站内容来自于网友发表,不代表本站立场,仅表示其个人看法,不对其真实性、正确性、有效性作任何的担保
相关事宜请发邮件给我们
© 星空见康网