Go Slice探秘——修改函数参数中的slice时,到底会不会改变原slice的值?

我们知道,Go中的slice是一个引用类型的值。
那么,当我们把slice当成一个函数参数传递之后,如果在函数中修改了该参数的值,会不会影响原来的slice呢?

一句话结论:

Go的slice类型中包含了一个array指针以及len和cap两个int类型的成员。

Go中的参数传递实际都是值传递,将slice作为参数传递时,函数中会创建一个slice参数的副本,这个副本同样也包含array,len,cap这三个成员。

副本中的array指针与原slice指向同一个地址,所以当修改副本slice的元素时,原slice的元素值也会被修改。但是如果修改的是副本slice的len和cap时,原slice的len和cap仍保持不变。

如果在操作副本时由于扩容操作导致重新分配了副本slice的array内存地址,那么之后对副本slice的操作则完全无法影响到原slice,包括slice中的元素。

下面,我们就通过几个例子来实际感受一下。

场景一:在函数中修改slice的成员的值

package main

import "fmt"   func main() {
   // myWeight是我7天的体重值序列
   myWeight := []int{11, 12, 13, 14, 15, 16, 17}
   fmt.Printf("myWeight: %v\n", myWeight)
   fmt.Printf("address of myWeight: %p    %p\n", myWeight, &myWeight)
   fmt.Printf("myWeight len: %v, cap: %v\n\n", len(myWeight), cap(myWeight))

   // 重置数据
   resetWeight(myWeight)
   fmt.Printf("myWeight after reset: %v\n", myWeight)
   fmt.Printf("address of myWeight after reset: %p    %p\n", myWeight, &myWeight)
   fmt.Printf("myWeight len: %v, cap: %v\n", len(myWeight), cap(myWeight))
}

func resetWeight(weight []int) {
   for i := 0; i < len(weight); i++ {
      weight[i] = weight[i] + i*10
  }
  fmt.Printf("address of weight: %p      %p\n\n", weight, &weight)
}
复制代码

运行结果如下:

myWeight: [11 12 13 14 15 16 17]
address of myWeight: 0xc000090040		0xc000058400
myWeight len: 7, cap: 7

address of weight: 0xc000090040		0xc000058480

myWeight after reset: [11 22 33 44 55 66 77]
address of myWeight after reset: 0xc000090040		0xc000058400
myWeight len: 7, cap: 7
复制代码

可以看到:
函数中修改了weight值之后,原来的myWeight序列也同样被修改了。

比较几次打印的地址我们可以看到:

原myWeight变量的地址是0xc000058400,它是一个slice,引用类型,指向的地址实际是0xc000090040。

而slice myWeight在作为函数参数传递时,实际上传递的是引用指向的地址0xc000090040,函数实际上另外开辟了一个临时变量weight来存放这个引用的值,新变量的地址是0xc000058480。

我们在resetWeight函数中修改slice weight中的值时,实际上修改的是weight指向的地址0xc000090040存放的内容。
而此时,函数外部的myWeight指向的地址同样是0xc000090040,因此,我们就看到slice myWeight的内容被修改了。

场景二:在函数中向slice添加成员

package main

import "fmt"   func main() {
   myWeight := make([]int, 1, 3)
   fmt.Printf("myWeight: %v\n", myWeight)
   fmt.Printf("address of myWeight: %p       %p\n", myWeight, &myWeight)
   fmt.Printf("myWeight len: %v, cap: %v\n\n", len(myWeight), cap(myWeight))

   // 添加数据
   addWeightRecord(myWeight)
   fmt.Printf("myWeight after add: %v\n", myWeight)
   fmt.Printf("address of myWeight after add: %p     %p\n", myWeight, &myWeight)
   fmt.Printf("myWeight len: %v, cap: %v\n", len(myWeight), cap(myWeight))
}

func addWeightRecord(weight []int) {
   weightCap := cap(weight)
   weight[0] = 10
   fmt.Printf("cap of weight: %v\n\n", weightCap)
   for i := 0; i < weightCap-1; i++ {
      weight = append(weight, i)
   }

   fmt.Printf("weight: %v\n", weight)
   fmt.Printf("address of weight: %p     %p\n", weight, &weight)
   fmt.Printf("weight len: %v, cap: %v\n", len(weight), cap(weight))
}
复制代码

运行结果如下:

myWeight: [0]
address of myWeight: 0xc0000600a0		0xc00005a400
myWeight len: 1, cap: 3

cap of weight: 3

weight: [10 0 1]
address of weight: 0xc0000600a0		0xc000058480
weight len: 4, cap: 6

myWeight after add: [10]
address of myWeight after add: 0xc0000600a0		0xc00005a400
myWeight len: 1, cap: 3
复制代码

这一次我们发现,在函数中修改slice weight中变量值后,外部的myWeight能获取到这个修改;在函数中向slice weight添加元素时,外部的myWeight却并没有随之增加元素。

这又是为什么呢?

我们来看一下go源码中对slice的定义:

go/src/runtime/slice.go文件

type slice struct {
  array unsafe.Pointer
  len   int
  cap   int 
}
复制代码

slice中其实包含了三个成员:
一个指针类型的array,一个int类型的len,以及一个int类型的cap。

在slice作为参数传递时,实际上是将原来的slice做了一个拷贝,函数中新的slice变量拿到的其实是一个指针类型和两个int类型的值。

当我们在函数中修改slice时,如果修改的是指针,原slice的array同样指向这个地址,就可以感知到这个修改。但如果修改的是int变量,原slice就无法感知到。

所以,我们这里在函数中把weight第一个变量改为10之后,myWeight中的第一个变量值也变成了10,因为这两个slice的array都是指向的同一个内存地址。

当我们向weight中添加元素时,weight的len会变大,但是myWeight的len却不会发生改变,它的长度仍然为1,只能读到第一个元素10。

场景三:在函数中向slice添加成员,并且超过了原来的cap容量

我们修改一下刚刚的addWeightRecord()方法:

func addWeightRecord(weight []int) {
   weightCap := cap(weight)
   weight[0] = 10
   fmt.Printf("cap of weight: %v\n\n", weightCap)
   for i := 0; i < weightCap-1; i++ {
      weight = append(weight, i)
   }
   fmt.Printf("weight: %v\n", weight)
   fmt.Printf("address of weight: %p     %p\n", weight, &weight)
   fmt.Printf("weight len: %v, cap: %v\n\n", len(weight), cap(weight))

   for i := 0; i < 3; i++ {
      weight = append(weight, i)
   }

   fmt.Printf("extended weight: %v\n", weight)
   fmt.Printf("address of extended weight: %p    %p\n", weight, &weight)
   fmt.Printf("extended weight len: %v, cap: %v\n\n", len(weight), cap(weight))
}
复制代码

运行结果如下:

myWeight: [0]
address of myWeight: 0xc0000600a0		0xc000058400
myWeight len: 1, cap: 3

cap of weight: 3

weight: [10 0 1]
address of weight: 0xc0000600a0		0xc000058480
weight len: 3, cap: 3

extended weight: [10 0 1 0 1 2]
address of extended weight: 0xc000074060		0xc000058480
extended weight len: 6, cap: 6

myWeight after add: [10]
address of myWeight after add: 0xc0000600a0		0xc000058400
myWeight len: 1, cap: 3
复制代码

这一组函数在对slice weight添加元素时,超过了weight的容量。这时候会为weight重新分配更大的内存,来存放更多的元素。

我们可以看到,这时候weight本身的地址并没有变化,仍然是0xc000058480,但是array指向的内存地址却发生了变化,从0xc0000600a0变成了0xc000074060。

在这一次运行中,只有在weight还没扩容时修改第一个元素为10的操作被原slice感知到了,其他操作对原来的slice都没有影响。


发表评论

电子邮件地址不会被公开。 必填项已用*标注