【Go】将切片作为参数传入函数并使用append方法遇到的问题_grom association append 入参-CSDN博客
切片的内部结构:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
由切片的结构定义可知,切片的结构由三个信息组成:
指针Data,指向底层数组中切片指定的开始位置
长度Len,即切片的长度
容量Cap,也就是最大长度,即切片开始位置到数组的最后位置的长度
问题:
将切片作为函数参数传入时,在函数内使用append方法并不能改变切片。
如下述代码所示:
func main() {
//创建一个长度和容量均为3的切片
arr := []int{1,2,3}
fmt.Println(arr) // [1 2 3]
//-------
addNum(arr)
//-------
fmt.Println(arr) // [1,2,3]
}
func addNum(sli []int){
//使用appedn添加"4"
sli = append(sli, 4)
fmt.Println(sli) // [1,2,3,4]
}
初步分析:
切片是作为值传递给函数的,而非引用传递,因此在函数中会创建一个拷贝切片,而拷贝切片指针也是指向原切片指针所指地址。
在函数中使用append方法,切片的底层数组进行了扩容处理,因此在拷贝切片中,指针指向了新的数组,而原切片并没有指向新的数组,因此原切片不会添加新的值。
测试如下:
func main() {
arr := []int{1,2,3}
fmt.Printf("%p\n",arr) //0xc000014150
//-------
addNum(arr)
//-------
fmt.Printf("%p\n",arr) //0xc000014150
}
func addNum(sli []int){
sli = append(sli, 4)
fmt.Printf("%p\n",sli) //0xc00000c360
}
从输出结果可看出,拷贝切片指针发生改变,而原切片指针没有变化。
一个例子:
此时创建一个长度为3,容量为4的切片。
再次使用append方法在函数中对切片进行添加操作。
代码如下:
func main() {
arr := make([]int,3,4)//创建一个长度为3,容量为4的切片
fmt.Printf("%p\n",arr) //0xc000012200
// -----
addNum(arr)
// -----
fmt.Printf("%p\n",arr) //0xc000012200
}
func addNum(sli []int){
sli = append(sli, 4)
fmt.Printf("%p\n", sli) //0xc000012200
}
从结果可以看出,因为初始时,已经设置了切片的容量为4,所以拷贝切片并没有因为扩容指向新的数组。
那此时原切片是否会发生改变?
func main() {
arr := make([]int, 3, 4) //创建一个长度为3,容量为4的切片
fmt.Printf("%p\n", arr) //0xc000012200
fmt.Println(arr) //[0 0 0]
// -------
addNum(arr)
// -------
fmt.Printf("%p\n", arr) //0xc000012200
fmt.Println(arr) //[0 0 0]
}
func addNum(sli []int) {
sli = append(sli, 4)
fmt.Printf("%p\n", sli) //0xc000012200
fmt.Println(sli) //[0 0 0 4]
}
从代码运行结果可以看出,原切片并没有发生改变。
因为,虽然原切片的底层数组发生了变化,但长度Len没有发生变化,因此原切片的值仍然不变。
func main() {
arr := make([]int, 3, 4) //创建一个长度为3,容量为4的切片
fmt.Println(arr, len(arr), cap(arr)) //[0 0 0] 3 4
// -----
addNum(arr)
// -----
fmt.Println(arr, len(arr), cap(arr)) //[0 0 0] 3 4
}
func addNum(sli []int) {
sli = append(sli, 4)
fmt.Println(sli, len(sli), cap(sli)) //[0 0 0 4] 4 4
}
另一个例子:
仍然将切片作为参数传入函数,在函数中修改切片的值。
代码如下:
func main() {
arr := []int{1, 2, 3, 4}
fmt.Println(arr) //[1 2 3 4]
// -----
editNum(arr)
// -----
fmt.Println(arr) //[666 2 3 4]
}
func editNum(sli []int) {
sli[0] = 666
fmt.Println(sli) //[666 2 3 4]
}
此时,从结果上,看起来切片的传参是采用引用传递。
但实际上,切片的传参是使用值传递。
函数能够对切片进行修改,是因为在函数中,拷贝切片所指的数组发生了变化,因此原切片的结果也发生变化。
总结
将一个切片作为函数参数传递给函数时,其实采用的是值传递,因此传递给函数的参数其实是切片结构体的值拷贝。因为Data是一个指向数组的指针,所以对该指针进行值拷贝时,得到的指针仍指向相同的数组,所以通过拷贝的指针对底层数组进行修改时,原切片的值也会发生相应变化。
但是,我们以值传递的方式传递切片结构体的时候,同时也是传递了Len和Cap的值拷贝,因为这两个成员并不是指针,因此,当函数返回时,原切片结构体的Len和Cap并没有改变。
所以可以解释如下现象:当传递切片给函数时,并且在函数中通过append方法向切片中增加值,当函数返回的时候,切片的值没有发生变化。
其实底层数组的值是已经改变了的(如果没有触发扩容的话),但是由于长度Len没有发生改变,所以显示的切片的值也没有发生改变。
如果需要在函数中进行append操作怎么办?
答:一个方法就是用指针。
实例代码如下:
func main() {
arr := []int{1, 2, 3, 4}
fmt.Println(arr) //[1 2 3 4]
// -----
addNum(&arr)
// -----
fmt.Println(arr) //[1 2 3 4 5]
}
func addNum(sli *[]int) {
*sli = append(*sli, 5)
fmt.Println(*sli) //[1 2 3 4 5]
}
补充信息:Go中没有引用传递
官方说明:
In a function call, the function value and arguments are evaluated in the usual order. After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution.
文档地址:https://golang.org/ref/spec#Calls
Go中函数调用只有值传递。
但网上有很多的说法,最多的是slice,map和chan作为参数传递到函数中时是传的引用,其实这个说法不准确,我们不能单纯因为函数内部的修改可以反馈到外面就认为是传递的引用。
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/bestzy6/article/details/119981699
-
总算说清楚了,从来就没想明白过这个数组啊,切片啊,传值传地址传指针,append是数组还是切片,是切片赋值给数组还是数组赋值给切片……感谢。
-
JasonCPLUS2023.10.17
1
其实没扩容、没改变地址也观测不到的原因,简单来说就是函数外slice的len没有改变,可以认为它只会观测到当前的长度。如果函数外强行按地址取值是能看到append后的值的
-
Jayj19972022.04.18
"""
在函数中使用append方法,切片的底层数组进行了扩容处理,因此在拷贝切片中,指针指向了新的数组,而原切片并没有指向新的数组,因此原切片不会添加新的值。
""" 那不扩容(cap)append就可以修改slice了吗?
"""
函数能够对切片进行修改,是因为在函数中,拷贝切片所指的数组发生了变化,因此原切片的结果也发生变化。
""" 不是说值传递吗 “拷贝切片所指的数组”与“原切片”?append不也是拷贝切片指向的数组发生变化吗
尤其是最后一句很经典:“补充信息:Go中没有引用传递”等于一句话把前面整篇文章全否了,不如抹除最后这句话 然后把之前的话修改一下,从开始就抛出:没有引用传递,而不是让看的人从开始就带着错误的观点吧。
下面是我认为的:
通过reflect.SliceHeader+unsafe.Pointer查看slice底层数组的指向,可以看到没有扩容时,append前后指向的是同一个数组
s1 := make([]int, 0, 2) s1 = append(s1, 1) s1hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s1)) fmt.Println("before", s1, len(s1), cap(s1), &s1[0], s1hdr) appendArr(s1) // 一个在函数内append了一次s1 fmt.Println("after", s1, len(s1), cap(s1), &s1[0], s1hdr)
得到同样的指针
&{824633811136 1 2}
&{824633811136 1 2}
可以看出其实函数内的append对slice的底层数组修改是成功了的,底层数组应该确实被append了一个值,而函数外的slice并不能因此修改len与cap导致slice无法观测到那个函数内被append的值,而不是什么更换了底层数组/值传递等原因;而index修改并没有影响slice的len与cap,只是修改了底层数组的值,没有改变指针,所以可以被观测到。 -
查看全部 5 条回复
-
sign_99回复Jayj19972023.08.19
没有发生cap被改变,开辟新的内存地址的情况下
-
-
xiaopengshen2021.12.07
棒棒
-
量化分析2021.11.07
但有个不懂的地方,这个如果采用的是值传递,那么不应该复制一个切片的struct结构体的副本么? 然后把新地址传入函数,这样里面的数组是地址,才可以公用被修改,而len cap因为是int,所以被复制的。
- 宅小2021.10.20
回复
实践出真知!赞一个