写点什么

一文看懂 slice

用户头像
Rayjun
关注
发布于: 2021 年 05 月 16 日
一文看懂 slice

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


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


先从 slice 的创建说起。

1. slice 的创建

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


带初始化元素的方式:

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


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

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


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

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


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

fmt.Println(nums[6]) // panic
复制代码


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

nums = nums[:len(nums)+1] // 将 slice 的长度扩展大 1nums = 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)) // 6fmt.Println(cap(numsSlice)) // 6
复制代码


2. slice 的结构

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


type Slice struct {  ptr      *T  len, cap int}
复制代码


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


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

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


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


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


3. 理解 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)) // 6fmt.Println(len(s1)) // 3
s2 := numsArr[3:5]fmt.Println(s2) // [4,5]fmt.Println(cap(s2)) // 3fmt.Println(len(s2)) // 2
复制代码


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


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


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


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


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


4. 理解 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)) // 3fmt.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)) // 3fmt.Println(len(s1)) // 2
s1 = append(s1, 1)
fmt.Println(s1[2]) //1
复制代码


5. 小结

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


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


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


文 / Rayjun

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

发布于: 2021 年 05 月 16 日阅读数: 582
用户头像

Rayjun

关注

程序员,王小波死忠粉 2017.10.17 加入

非著名程序员,还在学习如何写代码,公众号同名

评论

发布
暂无评论
一文看懂 slice