01介绍Golang语言中,函数参数和方法接收者可以选择使用值传递和指针传递(“引用传递”),需要注意的是,其中指针传递是传递的指针值的副本,而不是指针指向的数据。也就是说,Golang语言和C系列的所有语言一样,所有的传递都是值传递。在本文中,我们主要介绍方法的接收者如何选择使用值类型和指针类型。02方法接收者的类型选择在关键字type定义的类型上定义一个方法,方法的接收者也可以作为方法的参数,类似于函数的参数,所以方法的接收者是和函数参数一样,我们也需要考虑选择使用值类型和指针类型。对于这个问题,我们通常从两个方面来考虑。一是如果方法需要修改接收者,接收者必须使用指针类型;另一种是如果receiver占用内存大,出于性能考虑,我们也会选择使用指针类型的Receivers。此外,我们还需要考虑一致性。也就是说,如果该类型的某些方法必须使用指针类型的接收者,则其他方法也应该使用指针类型的接收者。所以不管这个类型怎么用,它的方法集都是一致的。最后,如果接收者是原始类型、切片和小型结构,它们的值类型内存占用低且易于阅读。因此,在这种情况下,除非方法的语义要求使用指针类型的接收者,否则我们可以选择使用值类型的接收者。typeUserstruct{namestring}func(uUser)SetNameValueType(strstring){fmt.Printf("SetNameValueType()pointer:%p\n",&u)//SetNameValueType()pointer:0xc000096240u.name=str}func(u*User)SetNamePointerType(strstring){fmt.Printf("SetNamePointerType()pointer:%p\n",u)//SetNamePointerType()pointer:0xc000096220u.name=str}funcmain(){user1:=&User{}fmt.Printf("pointer:%p\n",user1)//pointer:0xc000096220fmt.Println(user1)//&{}user1.SetNameValueType("lucy")fmt.Println(user1)//&{}user1.SetNamePointerType("lily")fmt.Println(user1)//&{lily}}阅读上面的代码,我们可以发现值类型的接收者,调用者拷贝了copy;指针类型的接收者,调用者没有复制副本。03复合类型映射和切片值类似于指针:它们是包含指向底层映射或切片数据的指针的描述符。复制映射或切片值不会复制它指向的数据。需要注意的是,如果超过了切片的容量,运行时会重新分配一个新的内存地址。map源码:typehmapstruct{countint//#livecells==sizeofmap.Mustbefirst(usedbylen()builtin)flagsuint8Buint8//log_2of#ofbuckets(canholduptoloadFactor*2^Bitems)noverflowuint16//overflowbuckets的近似值;参见incrnoverflowfordetailshash0uint32//hashseedbucketsBBarrayof.2.maybenilifcount==0.oldbucketsunsafe.Pointer//previousbucketarrayofhalfthesize,non-nilonlywhengrowingnevacuateuintptr//progresscounterforevacuation(bucketslessthanthishavebeenevacuated)extra*mapextra//optionalfields}切片源码:typeslicestruct{arrayunsafe.Pointerlenintcapint}说明{user=main()&代码{user()&代码}fmt.Printf("pointer:%p\n",user1)//pointer:0xc000096220fmt.Println(user1)//&{}user1.SetNameValueType("lucy")fmt.Println(user1)//&{}user1.SetNamePointerType("lily")fmt.Println(user1)//&{lily}//m:=make(map[int]int)m:=map[int]int{}fmt.Printf("mappointer:%p\n",m)//地图指针:0xc000100180m[0]=1fmt.Printf("mappointer:%p\n",m)//mappointer:0xc000100180m[1]=2s:=make([]int,0,1)fmt.Printf("slicepointer:%p\n",s)//slicepointer:0xc00001c0a0s=append(s,1)fmt.Printf("切片指针:%p\n",s)//切片指针:0xc00001c0a0s=append(s,2)fmt.Printf("切片指针:%p\n",s)//slicepointer:0xc00001c0b0}阅读上面的代码,我们可以发现map类型并没有分配新的内存地址,使用append函数向slice添加元素,当元素个数不超过其容量时,切片不分配新的内存地址关于接口类型,复制接口值复制接口值中存储的对象。如果接口值持有一个结构,复制接口值复制结构。如果接口值持有指针,copyingtheinterfacevalue复制了指针,但不会复制它指向的数据04如何避免复制值类型的副本读者读到这里可能会简单地认为使用值类型会复制副本,而使用pointer类型不会复制副本。其实我们可以优化代码,在不改变语义的情况下,实现使用值类型,不做拷贝。示例代码:typeUserstruct{namestring}func(uUser)SetNameValueType(strstring){fmt.Printf("SetNameValueType()pointer:%p\n",&u)//SetNameValueType()pointer:0xc000096240u.name=str}func(uUser)ValueSetName(strstring)User{u.name=strreturnu}funcmain(){user2:=&User{}fmt.Printf("user2pointer:%p\n",user2)//user2pointer:0xc000010290user2.SetNameValueType("tom")//SetNameValueType()pointer:0xc0000102a0user3:=&User{}fmt.Printf("user3pointer:%p\n",user3)//user3pointer:0xc0000102b0user3.ValueSetName("bob")fmt.Printf("pointer:%p\nn",user3)//pointer:0xc0000102b0}阅读上面的代码,我们发现User的SetNameValueType方法和ValueSetName方法,都是传值,但是SetNameValueType方法会复制copy,ValueSetName方法不会copythe复制。原因是我们为ValueSetName方法定义了一个User类型的返回值,从而避免了对ValueSetName方法的复制。05总结在这篇文章中,我们主要介绍了方法接收者使用值传递和指针传递的区别,描述了选择使用值传递和指针传递时需要考虑的决定因素,同时也指出了复合类型和复合类型的区别值类型。最后用一个简单的例子来演示如何在不改变语义的情况下优化代码,在不复制的情况下使用值类型。本文转载自微信公众号《Golang语言开发栈》,可通过以下二维码关注。转载本文请联系Golang语言开发栈公众号。
