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
,如果需要传递到函数外部,需要传指针。(或者使用函数返回值)