一个实验带你真正搞懂 Go 结构体方法指针和值的区别
众所周知,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:mainval:1
In main(): after f.SetP(1):val:1
=====================================
In SetV():call by:mainval:2
In main(): after f.SetV(2):val:1
如我们预期,通过值定义的方法内对属性的修改并不会把影响带到外部。
接下来,开始我们的实验
假如方法套娃,会发生什么? 在我们日常开发时,经常会遇到方法里调用另一个方法,那假如被调用的方法里修改了属性,会发生什么呢?
套娃会有四种情况:
PV
、VP
、VV
、PP
(实际情况可能还会出现更多层的套娃,但是这里我们只需要弄懂一层的,剩下可以按照数学归纳法去理解。),往代码中加四个 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:mainval:1
In main(): after f.SetP(1):val:1
=====================================
In SetV():call by:mainval:2
In main(): after f.SetV(2):val:1
=====================================
In SetV():call by:main->SetPV val:4
In SetPV(): call by:mainval:1
In main(): after f.SetPV(3):val:3
=====================================
In SetP():call by:main->SetVP val:5
In SetVP(): call by:mainval:5
In main(): after f.SetVP(4):val:3
=====================================
In SetV():call by:main->SetVV val:6
In SetVV(): call by:mainval:3
In main(): after f.SetVV(5):val:3
=====================================
In SetP():call by:main->SetPP val:7
In SetPP(): call by:mainval:7
In main(): after f.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 (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=0x14000124008
In SetP():call by:mainval:1
In main(): after f.SetP(1):val:1
====================================================
In SetV():&f=0x14000124010&f.val=0x14000124010
In SetV():call by:mainval: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:mainval: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:mainval: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:mainval: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:mainval:7
In main(): after f.SetPP(6):val:6
可以发现:
- 不管是
(f Foo)
还是(f *Foo)
,在方法内部,f
本身都是拷贝的 - 属性地址
2.1. 如果是指针方法,则属性地址继承于调用方
2.2. 如果是值方法,则属性地址是新开辟的空间地址
总结 在这篇文章终于结束之前,总结一下这个无聊的实验对我们的实际开发有哪些有意义的提示:
- 如果某个方法你需要对属性做临时修改(比如当前方法需要调用其他方法,而目标方法会读取属性值且你不被允许修改目标方法),那么你应该将这个方法定义为值传递的
- 如果你某个方法定义为值传递的了,那么切记,你在这个方法内直接或者套娃所做的一切修改都不会不会向上传递(作用到调用者那里),但是它会向下传递
【一个实验带你真正搞懂 Go 结构体方法指针和值的区别】
文章图片
推荐阅读
- 一个人的旅行,三亚
- 一个小故事,我的思考。
- 一个人的碎碎念
- 七年之痒之后
- 我从来不做坏事
- 异地恋中,逐渐适应一个人到底意味着什么()
- 迷失的世界(二十七)
- live|live to inspire 一个普通上班族的流水账0723
- 遗憾是生活的常态,但孝顺这件事,我希望每一个人都不留遗憾
- NO.38|NO.38 我不是嫁不出去,而是不想嫁