大家好,我是炸鱼。前几天在我们的Go交流群里,有朋友问“xxx是引用类型吗?”需要有人为此而战。即Go语言到底是按值传递(passingbyvalue)还是按引用传递(passingbyreference)?Go的官方定义这部分参考了《Whenarefunctionparametersbyvaluepassed?》在Go官方的FAQ中,内容如下。和C家族的所有语言一样,Go中的一切都是按值传递的。也就是说,函数总是得到传递的内容的副本,就好像有一个赋值语句将值赋给参数一样。例如:将一个int值传递给一个函数将得到一个int的副本。传递指针值会得到指针的副本,但不会得到它指向的数据。映射和切片的行为类似于指针:它们是包含指向底层映射或切片数据的指针的描述符。复制映射或切片值不会复制它指向的数据。复制接口值会复制存储在接口值中的内容。如果接口值包含一个结构,则复制接口值会复制该结构。如果接口值包含一个指针,则复制接口值会复制指针,但不会复制它指向的数据。重要的一点是,Go语言中的所有内容都是按值传递的,而不是按引用传递的。不要直接应用其他概念,你会犯先入为主的错误。按值传递和按值传递按值传递也称为按值传递。指的是在函数调用的时候复制实参传递给函数,这样如果在函数中修改了参数,实参不会受到影响。简单来说,值传递就是传递的是参数的副本,也就是副本。它本质上不能认为是一个东西,它不指向一个内存地址。情况一如下:funcmain(){s:="脑子炸了"fmt.Printf("主存地址:%p\n",&s)hello(&s)}funchello(s*string){fmt.Printf("hellomemoryaddress:%p\n",&s)}输出结果:mainmemoryaddress:0xc000116220hellomemoryaddress:0xc000132020我们可以看到main函数中变量s指向的内存地址为0xc000116220.hello函数传参后,内部输出内存地址为0xc000132020,两者发生了变化。基于此,我们可以得出结论,在Go语言中,确实是传值。这是否意味着在函数内修改值不会影响主函数?情况2如下:funcmain(){s:="脑子炸了"fmt.Printf("主存地址:%p\n",&s)hello(&s)fmt.Println(s)}funchello(s*string){fmt.Printf("hello内存地址:%p\n",&s)*s="炸鱼已入脑"}我们修改了hello函数中变量s的值,然后finally在main函数中我们输出的变量s的值是多少。是“炸鱼脑”还是“炸鱼脑”?输出结果:主存地址:0xc000010240hello内存地址:0xc00000e030炸鱼入脑输出结果为“炸鱼入脑”。这个时候大家可能又开始嘀咕了。建宇之前说的是Go语言只能传值,也验证过两者的内存地址是不一样的。为什么他的价值现在改变了?这就是为什么?因为“如果传递的值指向内存空间的地址,那么这个内存空间是可以修改的”。即这两个内存地址实际上是指针的指针,它们的根指向同一个指针,即指向变量s。因此,我们进一步修改变量s,得到输出“炸鱼已入脑”的结果。引用传递,也称为按引用传递,是指在调用函数时,将实参的地址直接传递给函数,那么函数中参数的修改都会影响到实参。在Go语言中,官方已经明确表示没有passbyreference,也就是没有passbyreference。所以借用一个简单的文字描述,如例子中,即使传入参数,最终输出的内存地址也是一样的。这时候最有争议的map和slice就有些疑惑了。可以看到Go语言中的map和slice类型是可以直接修改的。不是同一个内存地址,不就是引用吗?事实上,FAQ中有一个非常重要的提醒:“maps和slice的行为类似于指针,它们是包含指向底层map或slice数据的指针的描述符”。map对于地图类型,我们仔细看例子:funcmain(){m:=make(map[string]string)m["我的脑子是炸鱼"]="这次!"fmt.Printf("主存地址:%p\n",&m)hello(m)fmt.Printf("%v",m)}funchello(pmap[string]string){fmt.Printf("hello内存地址:%p\n",&p)p["我的脑子是炸鱼"]="记得点个赞!"}输出结果:主存地址:0xc00000e028hello内存地址:0xc00000e038确实是传值,那么修改后的地图结果应该是什么。既然是传值,那肯定是“这次!”吧?输出结果:map[脑子炸了:记得点个赞!】结果修改成功,“记得点赞!”是输出。这就尴尬了,为什么是传值,还可以达到类似引用的效果,可以修改为源值?这里的技巧是:funcmakemap(t*maptype,hintint,h*hmap)*hmap{}这是创建地图类型的底层运行时方法。请注意,它返回*hmap类型,这是一个指针。即通过Go语言封装map类型的相关方法,用户需要注意指针传递。也就是说,我们在调用hello方法的时候,相当于传入了一个指针参数hello(*hmap),和前面的值类型的case2类似。我们称这种情况为“引用类型”,但“引用类型”不等同于按引用传递,或按引用传递,还是有明显区别的。与Go语言中的map类型类似的是chan类型:funcmakechan(t*chantype,sizeint)*hchan{}效果相同。slice是切片类型,具体看例子:funcmain(){s:=[]string{"烤鱼","咸鱼","摸鱼"}fmt.Printf("主存地址:%p\n",s)hello(s)fmt.Println(s)}funchello(s[]string){fmt.Printf("hello内存地址:%p\n",s)s[0]="fryFish"}输出结果:主存地址:0xc000098180hello内存地址:0xc000098180[炸鱼、咸鱼、摸鱼]从结果来看,两者的内存地址相同,变量s的值也改成功了。这不是路过,炸鱼翻车了吗?注意两个细节:没有&是用来获取地址的。可以直接用%p打印出来。之所以上面两件事可以同时做,是因为标准库fmt对这块做了优化:func(p*pp)fmtPointer(valuereflect.Value,verbrune){varuuintptrswitchvalue.Kind(){casereflect.Chan,reflect.Func,reflect.Map,reflect.Ptr,reflect.Slice,reflect.UnsafePointer:u=value.Pointer()default:p.badVerb(verb)return}注意代码value.Pointer,标准库进行了特殊处理,直接对应值的指针地址,当然不需要取地址符号。标准库fmt之所以能够输出切片类型对应的值,也是在这里:func(vValue)Pointer()uintptr{...caseSlice:return(*SliceHeader)(v.ptr).Data}}typeSliceHeaderstruct{DatauintptrLenintCapint}其内部转换的Data属性就是Go语言中切片类型SliceHeader的运行时表示。当我们调用%p输出时,输出的是切片底层存储数组元素的地址。接下来的问题是:为什么切片类型可以直接修改源数据的值呢?其实原理和输出是一样的。Go语言在运行时,也会传递相应切片类型底层数组的指针,但需要注意的是,它使用的是指针的副本。严格来说是引用类型,还是值传递。精彩的?总结在今天的文章中,我们对Go语言的日经问题做了一个基本的解释和分析:“Go语言是按值传递(passingbyvalue)还是按引用传递(passingbyreference)”。另外,在业界,最容易混淆的类型是slice、map、chan等类型,都认为是“passbyreference”,所以Go语言中的xxx就是passbyreference。我们也对此进行了案例演示。这其实是一种错误的认知,因为:“如果过去传递的值指向内存空间的地址,那么这个内存空间是可以修改的”。它确实做了一个拷贝,但是也达到了可以通过各种方式(实际上是传递指针)修改源数据的效果,这是一种引用类型。Shihammer,Go语言只传值。如有任何问题,欢迎在评论区反馈和交流。最好的关系是相互成就。您的好评是创作炸鱼最大的动力。感谢您的支持。文章持续更新中,可微信搜索【脑补炸鱼】阅读,本文已收录在GitHubgithub.com/eddycjy/blog,欢迎Star提醒。可以加我微信cJY0728,我拉你进围棋读者交流群,与万千开发者交流技术。参考Go读者交流群函数参数什么时候传值?Java是按值传递还是按引用传递?Go语言参数传递是按值还是按引用
