GO语言基础--slice

176 阅读5分钟

深度解密slice

slice 中文就是切片,它和数组(array)很类似,可以用下标的方式进行访问,如果越界,就会产生 panic。但是它比数组更灵活,可以自动地进行扩容。

// runtime/slice.go
type slice struct {
	array unsafe.Pointer // 元素指针
	len   int // 长度 
	cap   int // 容量
}

slice 共有三个属性: 指针,指向底层数组; 长度,表示切片可用元素的个数,也就是说使用下标对 slice 的元素进行访问时,下标不能超过 slice 的长度; 容量,底层数组的元素个数,容量 >= 长度。在底层数组不进行扩容的情况下,容量也是 slice 可以扩张的最大限度。

image.png 底层数组是可以被多个slice同时指向的,因此对一个slice的元素进行操作,是可能影响到其他slice的。

slice 的创建

直接声明:var slice []int
new:slice := *new([]int)
字面量:slice := []int{1,2,3,4,5}
make:slice := make([]int, 5, 10)
从切片或数组“截取” :slice := array[1:5] 或 slice := sourceSlice[1:5]

直接声明:

创建出来的 slice 其实是一个 nil slice。它的长度和容量都为0。和nil比较的结果为true。 这里比较混淆的是empty slice,它的长度和容量也都为0,但是所有的空切片的数据指针都指向同一个地址 0xc42003bda0。空切片和 nil 比较的结果为false

他们的内部结构如图:

image.png

区别nil切片空切片
创建方式var s []intvar s = []int{}
长度00
容量00
和 nil 比较truefalse

nil 切片和空切片很相似,长度和容量都是0,官方建议尽量使用 nil 切片。

字面量

package main

import "fmt"

func main() {
	s := []int{0, 1, 2, 3, 5: 100}
	fmt.Println(s, len(s), cap(s))
}

运行结果:[0 1 2 3 0 100] 9 9

唯一值得注意的是上面的代码例子中使用了索引号,直接赋值,这样,其他未注明的元素则默认 0 值

make

make函数需要传入三个参数:切片类型,长度,容量。当然,容量可以不传,默认和长度相等。

package main

import "fmt"

func main(){
    slice := make([]int, 5, 10) // 长度为5,容量为10
    slice[2] = 2 // 索引为2的元素赋值为2
    fmt.Println(slice)
}

截取

截取也是比较常见的一种创建 slice 的方法,可以从数组或者 slice 直接截取,当然需要指定起止索引位置。

基于已有 slice 创建新 slice 对象,被称为 reslice。新 slice 和老 slice 共用底层数组,新老 slice 对底层数组的更改都会影响到彼此。基于数组创建的新 slice 对象也是同样的效果:对数组或 slice 元素作的更改都会影响到彼此。

值得注意的是,新老 slice 或者新 slice 老数组互相影响的前提是两者共用底层数组,如果因为执行 append 操作使得新 slice 底层数组扩容,移动到了新的位置,两者就不会相互影响了。所以,问题的关键在于两者是否会共用底层数组

截取操作方式如下:

date := [...]int{0,1,2,3,4,5,6,7}
slice := date[2:4:6] //date[low, high, max]

对 data 使用3个索引值,截取出新的 slice。这里 data 可以是数组或者 slicelow 是最低索引值,这里是闭区间,也就是说第一个元素是 data 位于 low 索引处的元素;而 high 和 max 则是开区间,表示最后一个元素只能是索引 high-1 处的元素,而最大容量则只能是索引 max-1 处的元素。

max >= high >= low

当 high == low 时,新 slice 为空。

还有一点,high 和 max 必须在老数组或者老 slice 的容量(cap)范围内。

看看下面一段代码:

package main

import "fmt"

func main() {
	slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s1 := slice[2:5]
	s2 := s1[2:6:7]

	s2 = append(s2, 100)
	s2 = append(s2, 200)

	s1[2] = 20

	fmt.Println(s1)
	fmt.Println(s2)
	fmt.Println(slice)
}

运行结果:

[2 3 20]

[4 5 6 7 100 200]

[0 1 2 3 20 5 6 7 100 9]

这个结果形成的过程是这样的:

slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5]
s2 := s1[2:6:7]

s1 从 slice 索引2(闭区间)到索引5(开区间,元素真正取到索引4),长度为3,容量默认到数组结尾,为8。 s2 从 s1 的索引2(闭区间)到索引6(开区间,元素真正取到索引5),容量到索引7(开区间,真正到索引6),为5。

image.png 现在向 s2 尾部追加一个元素 100:

s2 = append(s2, 100)

s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都可以看得到。

image.png

再次向 s2 追加元素200:

s2 = append(s2, 100)

这时,s2 的容量不够用,该扩容了。于是,s2 将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。

image.png

最后,修改 s1 索引为2位置的元素:

s1[2] = 20

这次只会影响原始数组相应位置的元素。它影响不到 s2 了。

image.png

打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。

slice 和数组的区别

slice 的底层数据是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。

数组是定长的,长度定义好之后,不能再更改。在 Go 中,数组是不常见的,因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int 和 [4]int 就是不同的类型。

而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。