在 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