在浏览studygolang的一篇帖子时,发现楼主提出了一个slice与append的问题,我一寻思,这玩意儿我之前研究过一会儿,肯定能解答,然后还没敲5个字就发现我也不明白了,然后再次去研究了一遍,终于觉得自己理解了,现在记录一下。
首先放出原帖子的代码:
package main
import "fmt"
func main() {
arr := make([]int, 3, 4) //创建一个长度为 3 ,容量为 4 的切片
fmt.Println(arr, len(arr), cap(arr)) //[0 0 0] 3 4
// -----
fmt.Printf("%p\n", arr)
addNum(arr)
// -----
fmt.Println(arr, len(arr), cap(arr)) //[0 0 0] 3 4
fmt.Printf("%p\n", arr)
}
func addNum(sli []int) {
fmt.Printf("%p\n", sli)
sli = append(sli, 4)
fmt.Println(sli, len(sli), cap(sli)) //[0 0 0 4] 4 4
fmt.Printf("%p\n", sli)
}
疑问就是,为什么addNum操作后,arr没有变成[0 0 0 4]呢?
还是得再重新回顾一下相关的知识点。
Golang中的传参都是值拷贝传递。
package main
import "fmt"
type TS struct {
Field int
}
func FunctionA(ts TS) {
ts.Field = 1
}
func FunctionB(ts *TS) {
ts.Field = 1
}
func main() {
ts := TS{
Field: 0,
}
FunctionA(ts)
fmt.Println(ts) //{0}
FunctionB(&ts)
fmt.Println(ts) //{1}
}
FunctionA参数为结构体类型,FunctionB参数为引用类型。传入的时候,分别拷贝了一个结构体副本和一个引用副本,副本对象与原对象是处在不同内存地址中的,所以修改结构体副本对原对象无法产生影响;但是,引用类型副本由于是和它的原对象指向同一个地址的,所以修改引用类型副本指向的对象就影响到了原引用对象。
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice 并不是一个引用类型,而是一个通常的结构体(查看runtime/slice.go),它包含了一个指针,指向底层数组的首地址,有效数据长度len,以及最大容量cap。
这三个成员属性共同描述了一个slice。当我们把一个slice作为参数传递时,也发生了slice结构体的值拷贝:一样的len、cap,当然还有指向同一个地址的array指针。看下面代码:
package main
import "log"
func main() {
var s = []int{1, 1, 1}
A(s)
log.Println(s) //[1, 0, 1]
}
func A(slice []int) {
slice[1] = 0
}
比如说,函数中把参数slice的第2个元素内容从1改成0时,操作的是底层数组的第二个元素,所以传参后在函数内部修改slice参数的元素值,也会影响外面创建并传入的s。
内置函数append用于给slice的尾部添加元素。下面的代码展示了append的作用:
var slice = make([]int, 3)
slice = []int{1, 2, 3}
log.Println("before:", slice) //[1, 2, 3]
slice = append(slice, 4)
log.Println("after:", slice) //[1, 2, 3, 4]
在go源码builtin/builtin.go中有append函数的解释:
// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
// slice = append(slice, elem1, elem2)
// slice = append(slice, anotherSlice...)
// As a special case, it is legal to append a string to a byte slice, like this:
// slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type
如果要增加元素(扩容)的slice有足够的cap(即添加了新元素后,新slice的len没有超过原来的cap),那么就不会重新创建底层数组;否则会重新创建底层数组(创建方式见runtime/slice.go中的函数growslice,主要涉及cap的增加策略,这里不再阐述)。
看下面的代码:
package main
import "log"
func main() {
var s = make([]int, 3, 3)
log.Printf("开始:s的变量地址:%p, s的底层数组首地址:%p\n", &s, s)
A(s)
log.Printf("调用A后:s的变量地址:%p, s的底层数组首地址:%p\n", &s, s)
log.Println("原始slice s:", s)
}
func A(slice []int) {
log.Printf("---A开始:参数slice的变量地址:%p, slice的底层数组首地址:%p\n", &slice, slice)
slice = append(slice, 9)
log.Println("A函数内的slice结果:", slice)
log.Printf("---A结束:参数slice的变量地址:%p, slice的底层数组首地址:%p\n", &slice, slice)
}
执行结果:
2021/11/17 11:07:49 开始:s的变量地址:0xc00000c080, s的底层数组首地址:0xc000016460
2021/11/17 11:07:49 ---A开始:参数slice的变量地址:0xc00000c0e0, slice的底层数组首地址:0xc000016460
2021/11/17 11:07:49 A函数内的slice结果: [0 0 0 9]
2021/11/17 11:07:49 ---A结束:参数slice的变量地址:0xc00000c0e0, slice的底层数组首地址:0xc000018150
2021/11/17 11:07:49 调用A后:s的变量地址:0xc00000c080, s的底层数组首地址:0xc000016460
2021/11/17 11:07:49 原始slice s: [0 0 0]
可以注意到,A函数内的slice参数的地址是和外面的s的地址不一样的,这印证了golang传参是拷贝传递; 另外,传参拷贝后的slice也是len=3, cap=3的切片,在对它进行append时,由于增加一个元素后,len变成了4,超过了原来的cap=3,所以底层数组被重新分配,底层数组的地址也就变了。
现在我们创建一个条件,创建原始切片时将容量改为4, 让s的容量充足,再来实验:
package main
import "log"
func main() {
var s = make([]int, 3, 4)
log.Printf("开始:s的变量地址:%p, s的底层数组首地址:%p\n", &s, s)
A(s)
log.Printf("调用A后:s的变量地址:%p, s的底层数组首地址:%p\n", &s, s)
log.Println("原始slice s:", s)
}
func A(slice []int) {
log.Printf("---A开始:参数slice的变量地址:%p, slice的底层数组首地址:%p\n", &slice, slice)
slice = append(slice, 9)
log.Println("A函数内的slice结果:", slice)
log.Printf("---A结束:参数slice的变量地址:%p, slice的底层数组首地址:%p\n", &slice, slice)
}
关于底层数组地址的情况,可以预见地没有再发生改变了:
2021/11/17 11:16:22 开始:s的变量地址:0xc00000c080, s的底层数组首地址:0xc000016460
2021/11/17 11:16:22 ---A开始:参数slice的变量地址:0xc00000c0e0, slice的底层数组首地址:0xc000016460
2021/11/17 11:16:22 A函数内的slice结果: [0 0 0 9]
2021/11/17 11:16:22 ---A结束:参数slice的变量地址:0xc00000c0e0, slice的底层数组首地址:0xc000016460
2021/11/17 11:16:22 调用A后:s的变量地址:0xc00000c080, s的底层数组首地址:0xc000016460
2021/11/17 11:16:22 原始slice s: [0 0 0]
到此,复现了原楼主的情况:既然两个切片的底层数组地址都一样,为什么slice是[0 0 0 9], 而s却是[0 0 0]呢?
其实答案很简单,首先注意到我在上面打印地址的时候,特地给底层数组打印时称之为“底层数组首地址”,然后结合slice的数据结构:哪些东西才能描述一个切片?当然是array指针、len和cap。没错,尽管slice和s两者的底层数组首地址一样,但是它们的len不一样(slice是4,s是3),所以它们的读取有效长度也不一样,从而使最终打印结果出现了偏差。
还有一点,之前一直有一个误区,就是把切片当成真引用类型看待了,认为传参后对它做的任何修改都会影响到原切片。现在看来这个“修改”指的只是修改元素的内容,并不包括append扩容。
个人拙见,如有不当,烦请赐教,十分感谢。