今天在复习Go函数时遇到这样的一个描述
在Go中,切片的本质是一个结构体,包含一个指向底层数组的指针(prt),长度(len),容量(cap)。所以,切片本身包含一个指针,将切片按值传递给函数,在函数内对其修改,影响将会传递到函数外。因为底层的数组被修改了。
但当对切片进行append()
操作,若是元素数量超过原有切片的容量,将会使得切片容量增大,这就是问题所在。扩容后的切片,本质上是产生一个新的底层数组。如果在函数内对切片添加元素导致扩容,会导致元素内的切片指向一个新的数组,但是函数外的切片仍然指向原来旧的数组,则将会导致影响无法传递到函数外。如果希望函数内对切片扩容作用于函数外,就需要以指针形式传递切片。
于是具有实践精神的我就想要了解函数的切片传递与指针传递究竟有什么区别,并且为什么 如果希望函数内对切片扩容作用于函数外,就需要以指针形式传递切片。
在开始之前,我们首先了解Go中的切片是一种特殊的数据结构,他基于底层的数组实现。
切片结构体:
type slice struct {
array unsafe.Pointer
len int
cap int
}
他由指向底层数组的指针、长度len、和容量cap组成。
更多切片和数组相关知识点请看【Go自学第二节】Go中的数组与切片区别;切片的底层逻辑
我们都知道对切片内容的修改会影响底层数组的值,我设计两个函数分别传入切片和切片指针对其进行修改,来测试输出结果能得到什么?
func modifySlice(innerSlice []string) {
innerSlice[0] = "b"
innerSlice[1] = "b"
fmt.Println(innerSlice)
}
func main() {
outerSlice := []string{"a", "a"}
modifySlice(outerSlice)
fmt.Print(outerSlice)
}
输出结果
[b b]
[b b]
传入指针
func modifySlice(innerSlice *[]string) {
(*innerSlice)[0] = "b"
(*innerSlice)[1] = "b"
fmt.Println(*innerSlice)
}
func main() {
outerSlice := []string{"a", "a"}
modifySlice(&outerSlice)
fmt.Print(outerSlice)
}
输出结果
[b b]
[b b]
在上面的例子中,两种函数传参类型得到的结果都一样,似乎没发现有什么区别。通过指针传递它看起来毫无用处,而且无论如何切片都是通过什么形式传递的,在两种情况下切片内容都得到了修改。
这印证了我们一贯的认知:函数内对切片的修改,将会影响到函数外的切片。但,真的是如此吗?
我设计了以下场景,有一个slice={1,2,3},我设计两个函数分别传入切片和切片指针对其进行扩容,来测试输出结果能得到什么。
func add(slice []int) {
slice = append(slice, 4, 5, 6, 7, 8)
}
func add1(a *[]int) {
*a = append(*a, 4, 5, 6, 7, 8)
}
func main() {
array := [3]int{1, 2, 3}
slice := array[:]
add(slice)
fmt.Println("slice:", slice)
a := &slice
add1(a)
fmt.Println("slice_pointer:", slice)
}
输出结果
slice: [1 2 3]
slice_pointer: [1 2 3 4 5 6 7 8]
我们可以看到,当我们传入的参数为切片时,并不会影响函数外切片的值,但是当我们传入的参数是切片指针时就可以同时修改函数切片外的值,这是为什么呢????
原来在Go函数中,函数的参数传递均是值传递。那么,将切片通过参数传递给函数,其实质是复制了slice
结构体对象,两个slice
结构体的字段值均相等。正常情况下,由于函数内slice
结构体的array
和函数外slice
结构体的array
指向的是同一底层数组,所以当对底层数组中的数据做修改时,两者均会受到影响。
但是存在这样的问题:如果指向底层数组的指针被覆盖或者修改(copy、重分配、append触发扩容),此时函数内部对数据的修改将不再影响到外部的切片,代表长度的len和容量cap也均不会被修改。
当切片的长度和容量相等时,发生append,就会触发切片的扩容。扩容时,会新建一个底层数组,将原有数组中的数据拷贝至新数组,追加的数据也会被置于新数组中。切片的array指针指向新底层数组。所以,函数内切片与函数外切片的关联已经彻底斩断,它的改变对函数外切片已经没有任何影响了。
如果你只想修改切片中元素的值,而不会更改切片的容量与指向,则可以按切片传递切片,否则你应该考虑按指针传递。
练习一:
func modifySlice(innerSlice []string) {
innerSlice[0] = "b"
innerSlice = append(innerSlice, "a")
innerSlice[1] = "b"
fmt.Println(innerSlice)
}
func main() {
outerSlice := []string{"a", "a"}
modifySlice(outerSlice)
fmt.Println(outerSlice)
}
练习二:
func modifySlice(innerSlice []string) {
innerSlice = append(innerSlice, "a")
innerSlice[0] = "b"
innerSlice[1] = "b"
fmt.Println(innerSlice)
}
func main() {
outerSlice:= make([]string, 0, 3)
outerSlice = append(outerSlice, "a", "a")
modifySlice(outerSlice)
fmt.Println(outerSlice)
}
练习一答案
[b b a]
[b a]
练习二答案
[b b a]
[b b]
在上述例子中我们使用切片指针传递时,可以修改函数外的slice值,它的写法是这样的
func add1(a *[]int) {
*a = append(*a, 4, 5, 6, 7, 8)
}
//等价于
func add1(a *[]int) {
slice := *a
slice = append(slice, 4, 5, 6, 7, 8)
*a = slice
}
如果我们把最后一行的
*a = slice
替换为
a = &slice
是否得到的结果会有变化,如果有变化请问是为什么?
答案是,结果会像传入切片一样不会发生变化,因为第二个方法替换的是指针变量本身,
也就是在函数的作用域内,其修改是有效
函数返回后,并不影响指针a所指向的值(别忘了,Go 参数传递是值传递嘛!)
至于第一种则是对指针解引用,修改了其指向的值
更多了解Go之如何在函数内修改指针
Golang中引用传递理解