一文看懂 slice

831 阅读5分钟

Go 语言中虽然有数组,但在代码中直接用的比较少,而是会间接的用到,slice 存储数据就是用的 数组,甚至可以认为数组是为了 slice 存在。Go 语言中的 slice 可以当做数组来使用,也可以当做其他语言中的 List 来使用。

slice 表示一个拥有相同类型元素的可变长的序列。可变长是 slice 最重要的一个特性,这是它和数组最不同的地方,既然 slice 是依靠数组而存在的,那么 slice 是如何做到可变长的呢,这篇文章就来聊一下。

先从 slice 的创建说起。

slice 的创建

slice 可以主动创建,也可从现有的数组中获取。

带初始化元素的方式:

nums := []int{0, 1, 2, 3, 4, 5}
fmt.Println(len(nums)) // 6
fmt.Println(cap(nums)) // 6

不带初始化元素的方式,下面的这种方式会自动填充零值:

nums := make([]int, 6) 
fmt.Println(len(nums)) // 6
fmt.Println(cap(nums)) // 6

还可以创建一个更大容量的 slice:

nums := make([]int, 6, 12) 
fmt.Println(len(nums)) // 6
fmt.Println(cap(nums)) // 12

在这里需要需要注意,虽然 slice 容量有 12,但是却不能直接访问 slice 长度范围外的元素,否则会出现 panic:

fmt.Println(nums[6]) // panic

要么通过扩展 slice 的长度,要么使用 append 函数来添加元素。

nums = nums[:len(nums)+1] // 将 slice 的长度扩展大 1
nums = append(nums, 1) // 使用 append 将 nums 的长度加 1

在使用上面的方式创建 slice 时,Go 会自动的创建一个底层数组来存储数据,slice 本身并不会存储数据。

还有一种方式是通过数组来创建 slice:

numsArr := [...]int{0, 1, 2, 3, 4, 5}


numsSlice := numsArr[:] // 这表示 slice 容纳数组的所有元素
fmt.Println(len(numsSlice)) // 6
fmt.Println(cap(numsSlice)) // 6

slice 的结构

一般 可以把 slice 看成是一个复合结构,结构如下,由三部分组成:指针、长度和容量:

type Slice struct {
    ptr      *T
    len, cap int
}

slice 本身不会存储任何数据,slice 通过指针指向底层数组的某一个位置,这个位置就是 slice 的初始位置。长度表示当前 slice 中的元素个数,容量表示从指针的位置到底层数组的最后一个位置。

slice 可变长的第一个原因就是可以通过改变指针的位置来改变 slice 的长度。比如下面这样:

numsSlice2 := numsSlice[1:3]
fmt.Println(len(numsSlice2)) // 2
fmt.Prinltn(cap(numsSlice2)) // 5

这个操作在 numsSlice 的基础上创建的一个新的 slice,其实就是移动了一下 slice 的指针。

另外 slice 不能使用 == 进行比较,因为 slice 的元素是非直接的,也就是 slice 本身不存储值,slice 甚至可以包含自身。另外因为 slice 的元素是靠底层的数组存储,所以当底层数组变动的时候,slice 读取到的值也会产生变化。如果 使用 == 来进行比较,可能会产生很多预料之外的结果。

理解 slice 的 cap

对于初学者来说,会把 slice 的长度和容量搞混。

先来看下面这段代码:

numsArr := [...]int{1, 2, 3, 4, 5, 6}

s1 := numsArr[0 : 3] 
fmt.Println(s1) // [1,2,3]
fmt.Println(cap(s1)) // 6
fmt.Println(len(s1)) // 3

s2 := numsArr[3:5]
fmt.Println(s2) // [4,5]
fmt.Println(cap(s2)) // 3
fmt.Println(len(s2)) // 2

我们可以发现,同样是从数组上来生成 slice,但是生成之后的 slice 的容量却不一样,在上面而我们说到了 slice 的初始位置靠指针来决定,s1 指针的位置在数组的最开始的位置。按照容量的定义,从指针到底层数组结束的地方,所以它的 容量还是 6。

而 s2 中,指针的位置是数组的第四个元素,长度是 3,容量也是 3。这是初学者最容易犯错的地方。

**长度表示 slice 中目前可访问的元素个数,容量则表示这个 slice 在不改变底层数组的情况下,最多个扩展到的长度。**如果要扩展 slice,使用下面的操作就可以:

s2 = s2[:3] // 把上面 s2 的长度从 2 扩展到 3
fmt.Println(s2[2]) // 访问第三个元素

这种方式是让 slice 可变长的第二种方式。

理解 append

append 方法也是让 slice 可变长的一种方式。

append 函数经常会配合 slice 一起使用,在使用 append 的时候,我们需要使用下面的语法:

nums = append(nums, 1)

而且这种做法是必须的,如果没有采用这种做法,可能会产生意料之外的结果。看下面的代码:

numsArr := [...]int{1, 2, 3, 4, 5, 6}

s1 := numsArr[3:5]
fmt.Println(cap(s1)) // 3
fmt.Println(len(s1)) // 2

_ = append(s1, 1)

fmt.Println(s1[2]) // panic

上面的代码在调用 append 之后,如果没有使用返回的 slice,而是直接使用原来的 slice,就会产生越界的错误。

因为 slice 本身虽然是可变长的,所以如果 slice 还有容量,那么每次添加元素,都需要扩展 slice 的长度,返回的也是一个新的 slice。

另外 slice 依赖的底层数组是固定长度,在使用 append 时,如果底层的数组不足以存储新的元素之后,就需要扩容,扩容之后就会产生一个新的 slice 返回。

可以理解 append 方法修改传入的 slice,而我们在调用 append 函数之后,就需要使用函数返回的结果。所以上面的代码需要修改为:

numsArr := [...]int{1, 2, 3, 4, 5, 6}

	s1 := numsArr[3:5]
	fmt.Println(cap(s1)) // 3
	fmt.Println(len(s1)) // 2

	s1 = append(s1, 1)

	fmt.Println(s1[2]) //1

小结

slice 是一个很有用的数据接口,既可以当数组用,也可以当 list 使用。在使用 slice 的过程中,一定要注意 slice 本身不存储数据,它只是在一个底层数组上,截取不同长度序列,可以说 slice 就是一个数组的子序列。

另外我们说到了 slice 可变长的几种方式,一种是通过移动 slice 的指针,改变 slice 的长度,第二种是 slice 容量范围内扩展长度,第三种是通过 append 方式,这种方式会创建一个新的 slice。

另外我们经常会对 string 做子串的截取操作和这里 slice 工作原理是一样的。

文 / Rayjun

本文首发于微信公众号【Rayjun】