20

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

 3 years ago
source link: https://studygolang.com/articles/29383
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我们知道,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都没有影响。

欢迎关注我们的微信公众号,每天学习Go知识

FveQFjN.jpg!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK