2、函数参数中的 type
2.1 基本数据类型
来看一个例子:
package main import "fmt" func main() { a, b := 0, 0 fmt.Printf("====== 初始化 ====== \n") fmt.Printf("Memory Location a: %p, b: %p\n", &a, &b) fmt.Printf("value a: %d, b: %d\n", a, b) // 0 0 Add(a) AddPtr(&b) fmt.Printf("\n ====== final ====== \n") fmt.Printf("Memory Location a: %p, b: %p\n", &a, &b) fmt.Printf("value a: %d, b: %d\n", a, b) // 0 1 } // 通过值传递 func Add(x int) { fmt.Printf("\n====== 'Add' 函数 ====== \n") fmt.Printf("Before Add, Memory Location: %p, Value: %d\n", &x, x) x++ fmt.Printf("After Add, Memory Location: %p, Value: %d\n", &x, x) } // 通过指针传递 func AddPtr(x *int) { fmt.Printf("\n ====== 'AddPtr' 函数 ====== \n") fmt.Printf("Before AddPtr, Memory Location: %p, Value: %d\n", x, *x) *x++ // We add * in front of the variable because it is a pointer, * will call value of a pointer fmt.Printf("After AddPtr, Memory Location: %p, Value: %d\n", x, *x) }
输出
====== 初始化 ====== Memory Location a: 0xc00010a000, b: 0xc00010a008 value a: 0, b: 0 ====== 'Add' 函数 ====== Before Add, Memory Location: 0xc00010a010, Value: 0 After Add, Memory Location: 0xc00010a010, Value: 1 ====== 'AddPtr' 函数 ====== Before AddPtr, Memory Location: 0xc00010a008, Value: 0 After AddPtr, Memory Location: 0xc00010a008, Value: 1 ====== final ====== Memory Location a: 0xc00010a000, b: 0xc00010a008 value a: 0, b: 1
在 Add 方法中,内存位置的值与 main() 中的 a 不一样,因为 Go 复制了 a 的值并初始化了一个新的内存位置,所以如果我们改变值 x++,a 仍然是 0。a 的最终输出是0,因为它是 Pass by value。
在 AddPtr 方法中,值的内存位置与 b 相同,所以我们知道它共享同一个 "容器",我们在 AddPtr 中对 x 所做的一切都会影响 b 的值,我们尝试在 *x++ 中增加 x 的值。b 的最终输出是1,这是因为通过引用传递而改变的。
其他基本数据类型如 int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, string, bool, byte, rune, Array, Structs。Array 和 Struct 与基本数据类型具有相同的属性。
2.2 引用数据类型
2.2.1 map 类型
对于 Map 类型来说,一来我们可以通过函数修改它的内容,二来它没有明显的指针。
func main() { users := make(map[int]string) users[1] = "user1" fmt.Printf("before modify: user:%v\n", users[1]) // before modify: user:user1 modify(users) fmt.Printf("after modify: user:%v\n", users[1]) // after modify: user:user2 } func modify(u map[int]string) { u[1] = "user2" }
我们都知道,值传递是一份拷贝,里面的修改并不影响外面实参的值,那为什么map在函数内部的修改可以影响外部呢?
通过查看源码我们可以看到,实际上make底层调用的是makemap函数,主要做的工作就是初始化hmap结构体的各种字段:
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap { //... }
通过查看src/runtime/hashmap.go源代码发现,make函数返回的是一个hmap类型的指针*hmap。也就是说map===*hmap。 现在看func modify(p map)这样的函数,其实就等于func modify(p *hmap),相当于传递了一个指针进来。
而对于指针类型的参数来说,只是复制了指针本身,指针所指向的地址还是之前的地址。所以对map的修改是可以影响到函数外部的。
2.2.2 chan 类型
chan类型本质上和map类型是一样的,这里不做过多的介绍,参考下源代码:
func makechan(t *chantype, size int64) *hchan { //... }
chan也是一个引用类型,和map相差无几,make返回的是一个*hchan。
2.2.3 slice 类型
而 map 和 chan 使用 make 函数返回的实际上是 *hmap和*hchan 指针类型,也就是指针传递。slice 虽然也是引用类型,但是它又有点不一样。
简单来说就是,slice 本身是个结构体,但它内部第一个元素是一个指针类型,指向底层的具体数组,slice 在传递时,形参是拷贝的实参这个 slice,但他们底层指向的数组是一样的,拷贝 slice 时,其内部指针的值也被拷贝了,也就是说指针的内容一样,都是指向同一个数组。
我们先看一个简单的例子,对slice的某一元素进行赋值:
type slice struct { array unsafe.Pointer len int cap int }
下面举个例子:
func main() { arr := make([]int, 0) arr = append(arr, 1, 2, 3) fmt.Printf("outer1: %p, %p\n", &arr, &arr[0]) modify(arr) fmt.Println(arr) // 10, 2, 3 } func modify(arr []int) { fmt.Printf("inner1: %p, %p\n", &arr, &arr[0]) arr[0] = 10 fmt.Printf("inner2: %p, %p\n", &arr, &arr[0]) } //输出: //outer1: 0x14000112018, 0x14000134000 //inner1: 0x14000112030, 0x14000134000 //inner2: 0x14000112030, 0x14000134000 //[10 2 3]
因为slice是引用类型,指向的是同一个数组。可以看到,在函数内外,arr 本身的地址 &arr 变了,但是两个指针指向的底层数据,也就是&arr[0]数组首元素的地址是不变的。所以在函数内部的修改可以影响到函数外部,这个很容易理解。
再来看另外一个稍微复杂的例子,函数内部使用append。这个会稍微不一样。
func main() { arr := make([]int, 0) //arr := make([]int, 0, 5) arr = append(arr, 1, 2, 3) fmt.Printf("outer1: %p, %p, len:%d, capacity:%d\n", &arr, &arr[0], len(arr), cap(arr)) //modify(arr) appendSlice(arr) fmt.Printf("outer2: %p, %p, len:%d, capacity:%d\n", &arr, &arr[0], len(arr), cap(arr)) fmt.Println(arr) } func appendSlice(arr []int) { fmt.Printf("inner1: %p, %p, len:%d, capacity:%d\n", &arr, &arr[0], len(arr), cap(arr)) //modify(arr) arr = append(arr, 1) fmt.Printf("inner2: %p, %p, len:%d, capacity:%d\n", &arr, &arr[0], len(arr), cap(arr)) //modify(arr) //&arr[0]的地址是否相等,取决于初始化slice的时候的capacity是否足够 }
这个问题就相对复杂的多了。
分两种情况:
1)make slice 的时候没有分配足够的capacity
arr := make([]int, 0) 像这种写法,那么输出就是:
outer1: 0x14000114018, 0x1400012e000, len:3, capacity:3 inner1: 0x14000114030, 0x1400012e000, len:3, capacity:3 inner2: 0x14000114030, 0x1400012c060, len:4, capacity:6 outer2: 0x14000114018, 0x1400012e000, len:3, capacity:3 [1 2 3]
虽然函数内部append的结果同样不影响外部的输出,但是原理却不一样。
不同点:
- 在内部调用
append的时候,由于cap 容量足够,所以不需要扩容,在原地址空间增加一个元素,底层数组的首元素地址相同。 - 回到函数外部,打印出来还是
[1 2 3],是因为外层的len是3,所以只能打印3个元素,实际上第四个元素的地址上已经有数据了。只不过因为len为3,所以我们无法看到第四个元素。
那正确的 append 应该是怎么样的呢:
appendSlice(&arr) func appendSlice(arr *[]int) { *arr = append(*arr, 1) }
传指针进去,这样拷贝的就是这个指针,指针指向的对象,也就是slice本身,是不变的,我们使用*arr可以对slice进行操作。
2.3 小结
- Go里面没有
引用传递,Go语言是值传递。 - 如果需要函数内部的修改能影响到函数外部,那么就传指针。
map/channel本身就是指针,是引用类型,所以直接传map和channel本身就可以。slice的赋值操作其实是针对slice结构体内部的指针进行操作,也是指针,可以直接传slice本身。slice的append 操作同时需要修改结构体的len/cap,类似于struct,如果需要传递到函数外部,需要传指针。(或者使用函数返回值)
