调用函数时,传入的参数的传值还是传引用,几乎是每种编程语言都会关注的问题.最近在使用golang的时候,由于传值和传引用的方式没有弄清楚,导致了BUG.
经过深入的尝试,终于弄明白了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对于这种类型的参数,表面上是传引用调用,我也被这个表面现象迷惑过…
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,将变量的地址打印出来
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了.
golang中的slice也是指针类型,所以和上面*Person的原因一样
代码是最好的解释,先观察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外的变量
map类型也是指针类型,所以原因和上面的*Person一样
同样,看代码
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