golang的传值调用和传引用调用
本文内容纲要:
-传值还是传引用
-golang本质上都是传值方式调用
-普通类型的参数
-struct指针,map,slice类型的参数
-既然都是传值调用,为什么f3内修改了*Person,会导致外面的t3改变
-既然都是传值调用,为什么f4内修改了[]string,会导致外面的t4改变
-为什么f4内对[]stringappend之后,没有导致外面的t4改变
-既然都是传值调用,为什么f5内修改了map,会导致外面的t5改变
-为什么f5内增加了map中元素,会导致外面的t5改变,没有像t4那样,只变修改的部分,不变新增的部分
-总结
传值还是传引用
调用函数时,传入的参数的传值还是传引用,几乎是每种编程语言都会关注的问题.最近在使用golang的时候,由于传值和传引用的方式没有弄清楚,导致了BUG.
经过深入的尝试,终于弄明白了golang的传值的传引用,尝试过程记录如下,供大家参考!
golang本质上都是传值方式调用
严格来说,golang中都是传值调用,下面通过例子一一说明
普通类型的参数
这里的普通类型,指的是int,string等原始的数据类型,这些类型作为函数参数时,都是传值调用.这个基本没什么疑问.
funcparam_ref_test01(){
vart1=0
vart2="000"
varf1=func(pint){
p+=1
}
varf2=func(pstring){
p+="-changed"
}
fmt.Printf(">>>调用前:t1=%dt2=%s\n",t1,t2)
f1(t1)
f2(t2)
fmt.Printf("<<<调用后:t1=%dt2=%s\n",t1,t2)
}
运行的结果:
>>>调用前:t1=0t2=000
<<<调用后:t1=0t2=000
struct指针,map,slice类型的参数
对于这种类型的参数,表面上是传引用调用,我也被这个表面现象迷惑过…
funcparam_ref_test02(){
typePersonstruct{
Namestring
Ageint
}
vart3=&Person{
Name:"test",
Age:10,
}
vart4=[]string{"a","b","c"}
vart5=make(map[string]int)
t5["hello"]=1
t5["world"]=2
varf3=func(p*Person){
p.Name="test-change"
p.Age=20
}
varf4=func(p[]string){
p[0]="aa"
p=append(p,"d")
}
varf5=func(pmap[string]int){
p["hello"]=11
p["hello2"]=22
}
fmt.Printf(">>>调用前:t3=%vt4=%vt5=%v\n",t3,t4,t5)
f3(t3)
f4(t4)
f5(t5)
fmt.Printf("<<<调用后:t3=%vt4=%vt5=%v\n",t3,t4,t5)
}
运行的结果:
>>>调用前:t3=&{test10}t4=[abc]t5=map[hello:1world:2]
<<<调用后:t3=&{test-change20}t4=[aabc]t5=map[hello:11hello2:22world:2]
从运行结果中,可以看出基本符合传引用调用的特征,除了t4的append没有生效之外
既然都是传值调用,为什么f3内修改了*Person,会导致外面的t3改变
改造下f3,将变量的地址打印出来
funcparam_ref_test03(){
typePersonstruct{
Namestring
Ageint
}
vart3=&Person{
Name:"test",
Age:10,
}
varf3=func(p*Person){
p.Name="test-change"
p.Age=20
fmt.Printf("参数p指向的内存地址=%p\n",p)
fmt.Printf("参数p内存地址=%p\n",&p)
}
fmt.Printf("t3指向的内存地址=%p\n",t3)
fmt.Printf("t3的内存地址=%p\n",&t3)
f3(t3)
}
运行的结果:
t3指向的内存地址=0xc00000fe20
t3的内存地址=0xc000010570
参数p指向的内存地址=0xc00000fe20
参数p内存地址=0xc000010578
从结果可以看出,t3和p都是指针类型,但是它们的内存地址是不一样的,所以这是一个传值调用.但是,它们指向的地址(0xc00000fe20)是一样的,所以通过p修改了指向的数据(*Person),t3指向的数据也发生了变化.
只要p的指向地址变化,就不会影响t3的变化了
varf3=func(p*Person){
p=&Person{}//这行会改变p指向的地址
p.Name="test-change"
p.Age=20
}
f3(t3)
可以试试看,只要加上上面代码中有注释的那行,调用f3就不会改变t3了.
既然都是传值调用,为什么f4内修改了[]string,会导致外面的t4改变
golang中的slice也是指针类型,所以和上面*Person的原因一样
为什么f4内对[]stringappend之后,没有导致外面的t4改变
代码是最好的解释,先观察append之后内存地址的变化,我们再分析
funcparam_ref_test04(){
vars=[]string{"a","b","c"}
fmt.Printf("s的内存地址=%p\n",&s)
fmt.Printf("s指向的内存地址=%p\n",s)
s[0]="aa"
fmt.Printf("修改s[0]之后,s的内存地址=%p\n",&s)
fmt.Printf("修改s[0]之后,s指向的内存地址=%p\n",s)
s=append(s,"d")
fmt.Printf("append之后,s的内存地址=%p\n",&s)
fmt.Printf("append之后,s指向的内存地址=%p\n",s)
}
运行的结果:
s的内存地址=0xc00008fec0
s指向的内存地址=0xc00016d530
修改s[0]之后,s的内存地址=0xc00008fec0
修改s[0]之后,s指向的内存地址=0xc00016d530
append之后,s的内存地址=0xc00008fec0
append之后,s指向的内存地址=0xc000096f00
首先,无论是修改slice中的元素,还是添加slice的元素,都不会改变s本身的地址(0xc00008fec0)其次,修改slice中的元素,不会改变s指向的地址(0xc00016d530),所有在f4中修改slice的元素,也会改变函数f4外面的变量最后,append操作会修改s指向的地址,append之后,s和函数f4外的变量已经不是指向同一地址了,所以append的元素不会影响函数f4外的变量
既然都是传值调用,为什么f5内修改了map,会导致外面的t5改变
map类型也是指针类型,所以原因和上面的*Person一样
为什么f5内增加了map中元素,会导致外面的t5改变,没有像t4那样,只变修改的部分,不变新增的部分
同样,看代码
funcparam_ref_test05(){
varm=make(map[string]int)
m["hello"]=1
m["world"]=2
fmt.Printf("m的内存地址=%p\n",&m)
fmt.Printf("m指向的内存地址=%p\n",m)
m["hello"]=11
fmt.Printf("修改m之后,m的内存地址=%p\n",&m)
fmt.Printf("修改m之后,m指向的内存地址=%p\n",m)
m["hello2"]=22
fmt.Printf("追加元素之后,m的内存地址=%p\n",&m)
fmt.Printf("追加元素之后,m指向的内存地址=%p\n",m)
}
运行的结果:
m的内存地址=0xc000010598
m指向的内存地址=0xc000151590
修改m之后,m的内存地址=0xc000010598
修改m之后,m指向的内存地址=0xc000151590
追加元素之后,m的内存地址=0xc000010598
追加元素之后,m指向的内存地址=0xc000151590
根据上面的分析经验,一目了然,因为无论是修改还是添加map中的元素,m指向的地址(0xc000151590)都没变,所以函数f5中map参数修改元素,添加元素之后,都会影响函数f5之外的变量.
注意这里并不是说map类型的参数就是传引用调用,它仍然是传值调用,参数map的地址和函数f5外的变量t5的地址是不一样的如果在函数f5中修改的map类型参数的指向地址,就会像传值调用那样,不影响函数f5外t5的值
funcparam_ref_test06(){
vart5=make(map[string]int)
t5["hello"]=1
t5["world"]=2
varf5=func(pmap[string]int){
fmt.Printf("修改前参数p指向的内存地址=%p\n",p)
fmt.Printf("修改前参数p内存地址=%p\n",&p)
p=make(map[string]int)//这行改变了p的指向,使得p和t5不再指向同一个地方
p["hello"]=11
p["hello2"]=22
fmt.Printf("修改后参数p指向的内存地址=%p\n",p)
fmt.Printf("修改后参数p内存地址=%p\n",&p)
}
fmt.Printf("t5指向的内存地址=%p\n",t5)
fmt.Printf("t5内存地址=%p\n",&t5)
fmt.Printf(">>>调用前:t5=%v\n",t5)
f5(t5)
fmt.Printf("<<<调用后:t5=%v\n",t5)
}
运行的结果:
t5指向的内存地址=0xc000151590
t5内存地址=0xc000010598
>>>调用前:t5=map[hello:1world:2]
修改前参数p指向的内存地址=0xc000151590
修改前参数p内存地址=0xc0000105a0
修改后参数p指向的内存地址=0xc000151650
修改后参数p内存地址=0xc0000105a0
<<<调用后:t5=map[hello:1world:2]
虽然是map类型参数,但是调用前后,t5的值没有改变.
总结
上面的尝试不敢说有多全,但基本可以弄清golang函数传参的本质.
- 对于普通类型(int,string等等),就是传值调用,函数内对参数的修改,不影响外面的变量
- 对于struct指针,slice和map类型,函数内对参数的修改之所以能影响外面,是因为参数和外面的变量指向了同一块数据的地址
- 对于struct指针,slice和map类型,函数的参数和外面的变量的地址是不一样的,所以本质上还是传值调用
- slice的append操作会改变slice指针的地址,这个非常重要!!!我曾经写了一个基于slice的排序算法在这个上面吃了大亏,调研很久才发现原因…
本文内容总结:传值还是传引用,golang本质上都是传值方式调用,普通类型的参数,struct指针,map,slice类型的参数,既然都是传值调用,为什么f3内修改了*Person,会导致外面的t3改变,既然都是传值调用,为什么f4内修改了[]string,会导致外面的t4改变,为什么f4内对[]stringappend之后,没有导致外面的t4改变,既然都是传值调用,为什么f5内修改了map,会导致外面的t5改变,为什么f5内增加了map中元素,会导致外面的t5改变,没有像t4那样,只变修改的部分,不变新增的部分,总结,
原文链接:https://www.cnblogs.com/wang_yb/p/12126884.html