2020-10-29更新
"切片是指向数组的指针"这句话是不对的。切片就是切片,有自己的属性和方法,只是借用了数组来存储实际的数据。
切片的数据结构大概为
type slice struct {
point int
len int
cap int
}
即,管理一个数组上以point指向元素的数组下标为起点,加上len长度为终点,最大扩容到cap长度为终点的一段数据。
举例:
func main() {
D := [10]int{0,1,2,3,4,5,6}
A := D[0:3:3]
B := D[2:5:9]
C := D[4:7]
fmt.Printf("A: %v\n", A)
fmt.Printf("B: %v\n", B)
fmt.Printf("C: %v\n", C)
fmt.Printf("D: %v\n", D)
}
注意:
- 数组没有指定值的位置会用默认值填充,此处即为0
- 创建切片时第三个参数表示切片容量到数组的哪个索引位置,没有就默认到数组末尾,这个下面有用处,所以分三种设置做对比
- 从数组取切片时是前闭后开区间,与通常规定操作一致。
运行输出:
A: [0 1 2]
B: [2 3 4]
C: [4 5 6]
D: [0 1 2 3 4 5 6 0 0 0]
修改切片内容如下:
func main() {
D := [10]int{0,1,2,3,4,5,6}
A := D[0:3]
B := D[2:5]
C := D[4:7]
A[2] = 999
B[2] = 666
fmt.Printf("A: %v\n", A)
fmt.Printf("B: %v\n", B)
fmt.Printf("C: %v\n", C)
fmt.Printf("D: %v\n", D)
}
运行输出:
A: [0 1 999]
B: [999 3 666]
C: [666 5 6]
D: [0 1 999 3 666 5 6 0 0 0]
可以看出所有修改都同步了,并且体现在数组D上。
再来看Append操作。Append是扩展切片的长度,但是如果长度超过了预设的容量,就需要换一个底层数组。看下面的程序:
func main() {
D := [10]int{0,1,2,3,4,5,6}
A := D[0:3:3]
B := D[2:5:7]
C := D[4:7]
A = append(A, 333)
B = append(B, 666)
C = append(C, 999)
fmt.Printf("A: %v\n", A)
fmt.Printf("B: %v\n", B)
fmt.Printf("C: %v\n", C)
fmt.Printf("D: %v\n", D)
}
运行输出:
A: [0 1 2 333]
B: [2 3 4 666]
C: [4 666 6 999]
D: [0 1 2 3 4 666 6 999 0 0]
可以看到A因为预设了[0:3:3]的原因,容量只有3,当前已满,再增加一个333,就切换了新的数组,所以A的修改只体现在自身,对B、数组D都没有影响。
而B的容量为7,C的容量为5,都有空间,所以修改体现在了数组D上。
将切片A扩展到容量4,但是增加两个元素:
func main() {
D := [10]int{0,1,2,3,4,5,6}
A := D[0:3:4]
B := D[2:5:7]
C := D[4:7]
A = append(A, 333)
A = append(A, 332)
B = append(B, 666)
C = append(C, 999)
fmt.Printf("A: %v\n", A)
fmt.Printf("B: %v\n", B)
fmt.Printf("C: %v\n", C)
fmt.Printf("D: %v\n", D)
}
运行输出:
A: [0 1 2 333 332]
B: [2 333 4 666]
C: [4 666 6 999]
D: [0 1 2 333 4 666 6 999 0 0]
可以看到添加333时还没有超出切片A的容量,所以333还在数组D上做修改,而添加332时已经超出了A的容量,A换了一个新的数组(现有数据0、1、2、333复制过去),并且在新数组添加332,而不影响原来的数组D。
为什么强调不要把切片理解为数组的指针呢?这里还有一个非常重要的问题。看代码:
func main() {
slice := make([]int, 2, 3)
for i := 0; i < len(slice); i++ {
slice[i] = i
}
fmt.Printf("slice: %v, addr: %p \n", slice, slice)
changeSlice(slice)
fmt.Printf("slice: %v, addr: %p \n", slice, slice)
}
func changeSlice(s []int){
s = append(s, 3)
s = append(s, 4)
s[1] = 111
fmt.Printf("func s: %v, addr: %p \n", s, s)
}
运行输出:
slice: [0 1], addr: 0xc0000a0140
func s: [0 111 3 4], addr: 0xc0000ca030
slice: [0 1], addr: 0xc0000a0140
把changeSlice的s[1] = 111
操作提前:
func main() {
slice := make([]int, 2, 3)
for i := 0; i < len(slice); i++ {
slice[i] = i
}
fmt.Printf("slice: %v, addr: %p \n", slice, slice)
changeSlice(slice)
fmt.Printf("slice: %v, addr: %p \n", slice, slice)
}
func changeSlice(s []int){
s[1] = 111
s = append(s, 3)
s = append(s, 4)
fmt.Printf("func s: %v, addr: %p \n", s, s)
}
运行输出:
slice: [0 1], addr: 0xc0000a0140
func s: [0 111 3 4], addr: 0xc0000ca030
slice: [0 111], addr: 0xc0000a0140
首先,从实参和形参的地址可以看出来,实参和形参是两个切片,传参过程中是复制关系,这个不重要,指针传递时也是这样。
第二,实参和形参指向同一个数组,这个也不重要,指针传递时形参和实参也是指向同一片内存区域。
但是上面的代码,先扩容,形参slice的底层数组更换了(相当于形参指针指向了新的内存区域,即给指针重新赋值,但是并没有显式进行这个操作,不深入了解切片可能看不出来),所以s[1] = 111
不会影响到实参slice的底层数组,修改也就不会体现在实参slice中。下面的代码,先修改,修改发生在形参slice的底层数组上,也是实参slice的底层数组。所以修改体现在实参slice中。
Append如果发生扩容,相当于修改了指针指向的内存区域。
更新的分割线,下面是以前的理解,是错误的
Go数组是值类型,赋值给其他数组和函数传参操作都会复制整个数组数据,如果数组特别大,传来传去浪费大量内存。所以Go搞出了一个切片类型,切片类型的变量是指向一个数组的指针。对切片的操作,就是操作底层的数组。
这倒是和java很象,java的数组不是基本类型,是引用类型。
我们知道切片初始化有3种方式。
第一种:
arr := [2]int{100, 200} // 指定了大小的就是数组
sli := []int{100, 200} // 没指定大小的就是切片
第二种:
sli := make(int64, 5, 10) // 第三个参数容量可以省略,等于第二个参数长度。底层数组大小等于切片容量
第三种:
arr := [5]int{1,2,3,4,5}
sli := arr[1:2] // 初始化切片sli,是数组arr的引用
切片有length和capacity两个概念,length小于等于capacity。capacity不够用了就扩容,扩容的本质是更换一个新的底层数组,反之,不扩容只增加length不会更换底层数组。
在第三种方式中,sli := arr[1:2]
其实隐含了sli := arr[1:2:5]
的意思,这里1、2、5都是数组下标,slice[ i : j : k]
的length就是j - i
,capacity就是k - i
,sli的底层数组就是arr,这时候对sli的修改就是对arr的修改。
这里也不绝对,通过append增加sli的length,直到达到capacity触发扩容,sli就会更换一个新的底层数组(arr不够用了),此后对于sli的修改就不会改变arr了。
所以,一般建议使用
sli := arr[1:2:2]
只要append就会切换到新的底层数组,不影响原来的数组。当然你如果没有append直接改变现有的值,还是会改变数组的值。
时刻记住切片是指向数组的指针。