将切片作为参数传入函数并使用append方法遇到的问题

切片的内部结构:

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中没有引用传递

Go中函数调用只有值传递。

但网上有很多的说法,最多的是slice,map和chan作为参数传递到函数中时是传的引用,其实这个说法不准确,我们不能单纯因为函数内部的修改可以反馈到外面就认为是传递的引用。

你可能感兴趣的:(Go语言,golang)