我们知道,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都没有影响。
有疑问加站长微信联系(非本文作者)