看完这篇,下次就不用怕问golang值传递还是引用传递的问题了#golang#
在 Golang 中,无论是函数参数还是返回值,皆采用值传递的方式,即会将实参复制一份作为函数形参。而对于成员函数中的函数接收者,比如下面代码中的x和y。type User struct {    Age int}// 值类型func (x User) Add() {    x.Age = x.Age + 1    fmt.Printf("x address : %p, value: %v\n", &x, x)}// 指针类型func (y *User) AddPtr() {    y.Age = y.Age + 1    fmt.Printf("y address:%p ,y value: %p, the value pointed to by the pointer: %v\n", &y, y, *y)}由于成员函数等价于第一个参数是函数接收者的普通函数,因此函数接收者与普通参数一样,也是采取值传递的方式。// 值类型func Add(x User) {    x.Age = x.Age + 1    fmt.Printf("x address : %p, value: %v\n", &x, x)}// 指针类型func AddPtr(y *User) {    y.Age = y.Age + 1    fmt.Printf("y address:%p ,y value: %p, the value pointed to by the pointer: %v\n", &y, y, *y)}既然是值传递,那么在函数内部访问形参对象,理论上不应改变函数外部实参对象的值才对。然而,当我们将 map、slice、chan 等类型作为参数时,在函数内对形参进行写操作,函数外部实参却受到了影响,这究竟是怎么回事呢?本文将介绍不同类型的参数在传递时,函数内部对形参的写操作是否可以影响到外部对象以及其原因。同时,本文还收集了参数传递的最佳实践和高频面试题,期望对您的工作和面试有所帮助。1 参数类型1.1 基本类型基本数据类型如 int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64、uintptr、float32、float64、string、bool、byte、rune、Array、Structs 作为函数参数时,在函数内部对参数进行写操作,无法影响外部对象,除非参数类型为指针类型。例如下面的例子:package mainimport "fmt"type User struct {    Age int}func main() {    a := User{Age: 18}    fmt.Printf("before add a address : %p, value: %v\n", &a, a)    Add(a)    fmt.Printf("after add a address : %p, value: %v\n", &a, a)    AddPtr(&a)    fmt.Printf("after addptr a address : %p, value: %v\n", &a, a)}// 通过值传递func Add(x User) {    x.Age = x.Age + 1    fmt.Printf("x address : %p, value: %v\n", &x, x)}// 通过指针传递func AddPtr(y *User) {    y.Age = y.Age + 1    fmt.Printf("y address:%p ,y value: %p, the value pointed to by the pointer: %v\n", &y, y, *y)}// 输出// before add a address : 0xc000018080, value: {18}// x address : 0xc000018088, value: {19}// after add a address : 0xc000018080, value: {18}// y address:0xc000012030 ,y value: 0xc000018080, the value pointed to by the pointer: {19}// after addptr a address : 0xc000018080, value: {19}在 Add 方法中,我们改变值 x.Age++,a.Age 仍然是 18。原因是x的内存地址与 main() 中的 a 不一样,x是由Go 复制a 的值并构造的新对象。在  AddPtr 方法中,y是指针类型变量,变量内容为0xc000018080,也就是a的地址,代表y指针所指向的对象是 a 。所以我们在 AddPtr 中对 y 所指向对象的赋值,实际上就是对函数外部变量a的操作,我们尝试y.Age++,a.Age 的值从18变成19。1.2 特殊类型对于map、chan和slice类型,在函数内部修改它的内容,可以影响到函数外部对象。map类型对于map类型参数,没有明显的指针但是却可以在函数内部改变函数外部map的值。比如我们在下面update函数内修改形参u[1]="u2",在main函数中可以观察到,users[1]的值从u1变成了u2。package mainimport "fmt"func main() {    users := make(map[int]string)    users[1] = "u1"    fmt.Printf("before update: user:%v\n", users[1]) // before update: user:u1    update(users)    fmt.Printf("after update: user:%v\n", users[1]) // after update: user:u2}func update(u map[int]string) {    u[1] = "u2"}既然值传递是一份拷贝,函数内的修改并不影响外面实参的值,那为什么map在函数内部的修改可以影响外部呢?实际上,当我们创建map对象时,golang底层给我们返回了一个hmap的指针,因此可以理解成map==*hmap,update(u map)这样的函数,其实就等价于update(u *hmap),相当于传递了一个指针进来。func makemap(t *maptype, hint int, h *hmap) *hmap {   }而对于指针类型的参数来说,只是复制了指针本身,指针所指向的地址还是之前的地址。所以对函数内部map的修改是可以影响到函数外部的。chan类型chan类型和map类型一样,当我们创建chan对象时,golang底层给我们返回了一个hchan的指针,因此函数内部可以通过指针访问影响到函数外部对象。func makechan(t *chantype, size int) *hchan {}slice类型和map、chan类型不一样,当我们创建slice类型对象时,golang给我们返回的不是一个指针,而是下面这个slice结构的对象。(想深入了解slice原理的,可以看往期文章Golang是如何实现动态数组功能的?Slice切片原理解析)type slice struct {    array unsafe.Pointer // 数组指针    len   int // slice长度,len函数    cap   int // slice容量}当把slice当作函数参数时,会copy一份slice结构作为形参,但是形参和实参底层的array指针指向的是同一个数组。因此当我们在函数内部给slice元素赋值时,会影响到函数外部对象。我们来看一个例子:package mainimport "fmt"func main() {    a := make([]int, 0)    a = append(a, 1, 2)    fmt.Printf("outer1: %p, %p\n", &a, &a[0]) // outer1: 0xc0000a0018, 0xc0000a6020    update(a)    fmt.Println(a) // [3 2]}func update(b []int) {    fmt.Printf("inner1: %p, %p\n", &b, &b[0]) // inner1: 0xc0000a0030, 0xc0000a6020    b[0] = 3    fmt.Printf("inner2: %p, %p\n", &b, &b[0]) // inner2: 0xc0000a0030, 0xc0000a6020}//输出:// outer1: 0xc0000a0018, 0xc0000a6020// inner1: 0xc0000a0030, 0xc0000a6020// inner2: 0xc0000a0030, 0xc0000a6020// [3 2]在上面的例子中,a和b的地址分别是0xc0000a0018和0xc0000a0030,b是由实参a拷贝而来。a[0]和b[0]的地址相同,都是0xc0000a6020,说明a和b内部的指针成员变量array指向的是同一个数组。可以观察到在update函数内部给b[0]赋值为3,在main函数中,a[0]也变成了3。当我们在函数内部对slice类型的参数,执行append操作,对函数外部对象又有什么影响呢?答案是没有影响。我们分两种情况看一下。当make创建的slice容量不够时,在函数内部append操作,会发生扩容。函数外部对象所能看到的数据不变,函数内部形参底层数组地址发生迁移。比如下面的例子,在函数内部对象b append前后,函数外部切片a的地址、底层数组地址、len和cap都不变;函数内部对象b的地址不变,但是底层数组地址从0xc0000a6020变成了0xc0000b4020,len和cap也发生了变化。package mainimport "fmt"func main() {    a := make([]int, 0)    a = append(a, 1, 2)    fmt.Printf("before append a: %p, %p, len:%d, capacity:%d\n", &a, &a[0], len(a), cap(a))    appendSlice(a)    fmt.Printf("after append a: %p, %p, len:%d, capacity:%d\n", &a, &a[0], len(a), cap(a))    fmt.Println(a)}func appendSlice(b []int) {    fmt.Printf("before append b: %p, %p, len:%d, capacity:%d\n", &b, &b[0], len(b), cap(b))    b = append(b, 3)    fmt.Printf("after append b: %p, %p, len:%d, capacity:%d\n", &b, &b[0], len(b), cap(b))}// 输出// before append a: 0xc0000a0018, 0xc0000a6020, len:2, capacity:2// before append b: 0xc0000a0030, 0xc0000a6020, len:2, capacity:2// after append b: 0xc0000a0030, 0xc0000b4020, len:3, capacity:4// after append a: 0xc0000a0018, 0xc0000a6020, len:2, capacity:2// [1 2]当make创建的slice容量足够时,在函数内部执行append操作,不会发生扩容。函数外部对象所能看到的数据不变,函数内部形参底层数组地址也不会发生迁移,和外部对象仍然共享一个数组。比如下面的例子,在函数内部对象b append前后,函数外部切片a的地址、底层数组地址、len和cap都不变。函数内部对象b的地址不变,底层数组地址也不变。实际上a底层数组的第三个元素的地址上已经有数据了。只不过因为len为2,所以我们无法看到第三个元素。package mainimport "fmt"func main() {    a := make([]int, 0, 3)    a = append(a, 1, 2)    fmt.Printf("before append a: %p, %p, len:%d, capacity:%d\n", &a, &a[0], len(a), cap(a))    appendSlice(a)    fmt.Printf("after append a: %p, %p, len:%d, capacity:%d\n", &a, &a[0], len(a), cap(a))    fmt.Println(a)}func appendSlice(b []int) {    fmt.Printf("before append b: %p, %p, len:%d, capacity:%d\n", &b, &b[0], len(b), cap(b))    b = append(b, 3)    fmt.Printf("after append b: %p, %p, len:%d, capacity:%d\n", &b, &b[0], len(b), cap(b))}// 输出// before append a: 0xc0000a4018, 0xc0000ae018, len:2, capacity:3// before append b: 0xc0000a4030, 0xc0000ae018, len:2, capacity:3// after append b: 0xc0000a4030, 0xc0000ae018, len:3, capacity:3// after append a: 0xc0000a4018, 0xc0000ae018, len:2, capacity:3// [1 2]2 最佳实践函数参数和函数接收者什么时候该传值,什么时候该传指针?下面是golang官方建议。对于map、chan、func类型,实际上golang底层传递的就是指针,因此传参时不用额外传指针。对于slice类型,如果被调函数内部操作不会触发reslice,比如append操作,则不用传指针。否则需要传指针或者将函数内部slice返回。如果函数内部需要通过修改形参对实参生效,传指针。如果参数是包含sync.Mutex或其它类似同步字段的结构体,为了避免并发类型成员变量复制,传指针。对于大结构体或大数组,考虑到内存分配开销,传指针。如果接收者是结构体、数组或切片,并且它的任何元素是指向可能会被修改的对象的指针,那么倾向于使用指针接收者,因为这会使函数调用者更清楚其意图。如果接收者是一个小型数组或结构体等基础类型,内部成员变量没有特殊类型且没有指针,或者只是一个简单的基本类型,如int或string,那么值接收者是有意义的。值接收者可以减少gc扫描的对象数量(栈上而不是在堆上分配内存)。不要混合接收者类型。对于同一个结构体类型,要么全部方法都定义成指针接收者,要么全部定义成值类型接收者。不知道该用什么的场景,都用指针类型。3 高频面试题golang是值传递还是引用传递,解释原因。调用函数传入结构体时,应该传值还是指针?(Golang 都是值传递)slice底层实现,传参的时候传的是什么?如果map的value是基础类型,map 取一个 key,然后修改对应value的属性,原 map 数据的value会不会变化?不会变化,从map取出value的操作,在Go底层对应一个函数,该函数的返回值类型是value类型。因此,如果value是基本类型,函数返回值是原value的拷贝,如果需要修改原value,map的value类型可以是指针类型。比如下面的例子,usersStruct的value类型是结构体类型,直接usersStruct["a"].Age = 19编译不通过,且userA和usersStruct["a"]的值也不相等。usersPtr的value类型是指针类型,userB和usersPtr["b"]指向的是同一个对象,Age值相等。package mainimport "fmt"type User struct {    Age int}func main() {    usersStruct := make(map[string]User, 0)    usersStruct["a"] = User{Age: 18}    // usersStruct["a"].Age = 19 编译不通过,cannot assign to struct field usersStruct["a"].Age    userA := usersStruct["a"]    userA.Age = 19    fmt.Printf("userA age:%v,usersStruct[a] age:%v\n", userA.Age, usersStruct["a"].Age)    usersPtr := make(map[string]*User, 0)    usersPtr["b"] = &User{Age: 18}    userB := usersPtr["b"]    userB.Age = 19    fmt.Printf("userB age:%v,usersPtr[b] age:%v\n", userB.Age, usersPtr["b"].Age)}// 输出// userA age:19,usersStruct[a] age:18// userB age:19,usersPtr[b] age:19
点赞 2
评论 1
全部评论

相关推荐

Yushuu:你的确很厉害,但是有一个小问题:谁问你了?我的意思是,谁在意?我告诉你,根本没人问你,在我们之中0人问了你,我把所有问你的人都请来 party 了,到场人数是0个人,誰问你了?WHO ASKED?谁问汝矣?誰があなたに聞きましたか?누가 물어봤어?我爬上了珠穆朗玛峰也没找到谁问你了,我刚刚潜入了世界上最大的射电望远镜也没开到那个问你的人的盒,在找到谁问你之前我连癌症的解药都发明了出来,我开了最大距离渲染也没找到谁问你了我活在这个被辐射蹂躏了多年的破碎世界的坟墓里目睹全球核战争把人类文明毁灭也没见到谁问你了😆
点赞 评论 收藏
分享
头像
11-27 14:28
长沙理工大学
刷算法真的是提升代码能力最快的方法吗? 刷算法真的是提升代码能力最快的方法吗?
牛牛不会牛泪:看你想提升什么,代码能力太宽泛了,是想提升算法能力还是工程能力? 工程能力做项目找实习,算法也分数据结构算法题和深度学习之类算法
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务