Go语言 值传递
官方说法,Go中只有值传递,没有引用传递
而Go语言中的一些让你觉得它是引用传递的原因,是因为Go语言有值类型
和引用类型
,但是它们都是值传递
。
值类型 有int、float、bool、string、array、sturct等
引用类型有slice,map,channel,interface,func等
值类型:内存中变量存储的是具体的值。 比如: var num int 。num存放的是具体的int值,但是变量在内存中的地址可以通过 &num 来获取。
引用类型:变量直接存放的就是一个地址值,这个地址值指向的空间存的才是值。
代码测试:
func main() {
slice := []int{1, 2, 3}
arr := [2]int{1, 2}
m := make(map[string]string)
a := 13
var i *int = &a
ch := make(chan string)
fmt.Printf("[main array] %p\n", &arr)
fmt.Printf("[main pointer] %p\n", &i)
fmt.Printf("[main map] %p\n", &m)
fmt.Printf("[main slice] %p\n", &slice)
fmt.Printf("[main chan] %p\n", &ch)
fmt.Printf("[main slice 第一个元素的地址: ] %p\n", &slice[0])
fmt.Println()
get(arr, slice, m, i, ch)
}
func get(arr [2]int, s []int, m map[string]string, i *int, ch chan string) {
fmt.Printf("[main array] %p\n", &arr)
fmt.Printf("[get pointer] %p\n", &i)
fmt.Printf("[get map] %p\n", &m)
fmt.Printf("[get slice] %p\n", &s)
fmt.Printf("[get chan] %p\n", &ch)
fmt.Printf("[get slice 第一个元素的地址: ] %p\n", &s[0])
}
测试结果:
可以发现,数组、slice、map、chan、指针在传递过程中,地址都发生了变化。这说明传递的是一份拷贝。这里需要特意强调切片的第一个元素的地址前后没有发生改变。
但是我们在日常写go代码时发现,在函数里修改slice、map,函数外的值也会改变,这是为什么呢?
那接下来就逐个分析下。源码版本是1.21.3,这里就只是查看下源码创建slice,map时的返回值而已,不会讲解过多的源码内容。
引用类型分析
slice
slice 是一个长度可变的连续数据序列,其是个结构体,其中包含的字段包括:指向内存空间地址起点的指针 array、一个表示了存储数据长度的 len 和分配空间长度的 cap。
type slice struct {
array unsafe.Pointer
len int
cap int
}
func makeslice(et *_type, len, cap int) unsafe.Pointer {
.....................
return mallocgc(mem, et, true)
}
创建slice时候,返回的是数组地址。
那么 slice 在传递过程中,本质上传递的是 slice实例中的内存地址 array。
因为slice
是引用类型,指向的是同一个数组。也通过前面的测试代码结果,可以看到,在函数内外,slice本身的地址&
slice变了,但是两个指针指向的底层数据,也就是&
slice[0]
数组首元素的地址是不变的。
所以在函数内部的修改可以影响到函数外部,这个很容易理解。
那再来看看对slice使用append。代码如下
func main() {
arr := make([]int, 0) //容量cap不够的情况
// arr := make([]int, 0, 5) //容量cap足够的情况
arr = append(slice, 2, 4)
fmt.Printf("main1 slice地址:%p, 底层数组地址:%p ,len:%d, cap:%d\n", &arr, &arr[0], len(arr), cap(arr))
appendSlice(arr)
fmt.Printf("main2 slice地址:%p, 底层数组地址:%p ,len:%d, cap:%d\n", &arr, &arr[0], len(arr), cap(arr))
}
func appendSlice(arr []int) {
fmt.Printf("传递参数后,append前 slice地址:%p, 底层数组地址:%p ,len:%d, cap:%d\n", &arr, &arr[0], len(arr), cap(arr))
arr = append(arr, 1)
fmt.Println()
fmt.Printf("append后 slice地址:%p, 底层数组地址:%p ,len:%d, cap:%d\n", &arr, &arr[0], len(arr), cap(arr))
}
主要就有两种情况:
切片make不够容量
即是append时需要扩容
1.首先,外部传入一个slice,引用类型。
2.也还是值传递(slice地址发生了改变),但是两个arr指向的底层数组首元素&arr[0]没有改变
,也就是array unsafe.Pointer不变。
3.在内部调用append,因为cap容量不够,要扩容,
重新在新的地址空间分配底层数组,所以数组首元素的地址改变了。
4.回到函数外部,外部的slice指向的底层数组为原数组,内部的修改不影响原数组。
切片make够容量
结果一样是[2 4],虽然函数内部append
的结果同样不影响外部的输出,但是原理却不一样。
不同之处:
- 在内部调用
append
的时候,由于cap容量
足够,所以不需要扩容,在原地址空间增加一个元素即可,所以底层数组的首元素地址相同。 - 回到函数外部,打印出来还是
[2 4]
,是因为外层的len
是2,所以只能打印2个元素,实际上第3个元素的地址上已经有数据了。只不过因为len
为2,所以我们无法看到第3个元素。
不管cap容量够不够,其都没有改变外部slice的len和cap,所以最终看到的slice的len和cap都是没有改变的。
想要改变长度的的话,要传slice的指针。
传指针进去,拷贝的就是这个指针。指针指向的对象,也就是slice本身,是不变的。
func appendSlicePointer(arr *[]int) {
*arr = append(*arr, 5)
}
map
map使用的时候都是通过make来创建,例如
mymap := make(map[int]string)
通过查看源码我们可以看到,实际上make
底层调用的是makemap
函数。而在src/runtime/hashmap.go
源代码305行发现,makemap
函数返回的是一个hmap
类型的指针*hmap
。也就是说map===*hmap
。hmap是个结构体。
// makemap implements Go map creation for make(map[k]v, hint).
func makemap(t *maptype, hint int, h *hmap) *hmap {
.......................
return h
}
而对于指针类型的参数来说,只是复制了指针本身,指针所指向的地址还是之前的地址。所以对map的修改是可以影响到函数外部的。
chan
管道的创建
channel := make(chan int, 5)
通过查看src/runtime/chan.go
源代码72行发现,makechan
函数返回的是一个hchan类型的指针*hchan。hchan也是个结构体。所以chan类型和map类型是本质是一样的。
func makechan(t *chantype, size int) *hchan {
.................
var c *hchan
.....................
return c
}
总结:
1.Go中只有值传递,没有引用传递
2.如果需要函数内部的修改能影响到函数外部,那就传指针
3.map/chan本身是指针,是引用类型,直接传其本身即可
4.slice 在传递过程中,本质上传递的是其内存地址 array,也即是指针,直接传slice本身即可
5.slice的append操作需要修改结构体的len或者cap
,类似于struct。若要影响到函数外部,需要传指针,或通过函数返回值返回结果