本文主要介绍切片(slice)和它的基本使用。
因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性。在实际应用中切片更加多。
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址
、长度
和容量
。切片一般用于快速地操作一块数据集合。
|
|
指针指向切片元素对应的底层数组元素的地址。长度对应切片中元素的数目,长度不能超过容量。容量一般是从切片的开始位置到底层数据的结尾位置的长度。
切片的初始化
|
|
切片拥有自己的长度和容量,我们可以通过使用内置的len()
函数求长度,使用内置的cap()
函数求切片的容量。
切片表达式
切片表达式从字符串
、数组
、指向数组或切片的指针
中构造子字符串或切片。
它有两种变体:一种指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外还指定容量的完整的形式。
简单切片表达式
切片的底层就是一个数组,所以我们可以基于数组通过切片表达式得到切片。 切片表达式中的low
和high
表示一个索引范围(左包含,右不包含)。
|
|
用这种切片表达式要考虑索引合法的问题,否则会出现索引越界(out of range)的问题。
索引的上限是cap(s)而不是len(s)
完整切片表达式
对于数组,指向数组的指针,或切片(注意不能是字符串)支持完整切片表达式:
|
|
它会将得到的结果切片的容量设置为max-low
。在完整切片表达式中只有第一个索引值(low)可以省略;它默认为0。
|
|
输出结果:
|
|
使用 make() 构造切片
如果需要动态的创建一个切片,我们就需要使用内置的make()
函数。
|
|
其中:
- T:切片的元素类型
- size:切片中元素的数量
- cap:切片的容量
|
|
切片值复制与数据引用
数组的复制是值复制,如下例中,对于数组a的副本c的修改不会影响到数组a。然而,对于切片b的副本d的修改会影响到原来的切片b。这说明切片的副本与原始切片共用一个内存空间。
|
|
在Go语言中,切片的复制其实也是值复制,但这里的值复制指对于运行时SliceHeader结构的复制。
底层指针仍然指向相同的底层数据的数组地址,因此可以理解为数据进行了引用传递。切片的这一特性使得即便切片中有大量数据,在复制时的成本也比较小,这与数组有显著的不同。
扩容
Go语言的内建函数append()
可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。
|
|
注意:通过var声明的零值切片可以在append()
函数直接使用,无需初始化。
|
|
没有必要像下面的代码一样初始化一个切片再传入append()
函数使用,
|
|
每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()
函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
切片的扩容策略
|
|
初始检查:
-
如果新长度大于当前容量的两倍,直接返回新长度。
这是为了处理需要大幅增长的情况,避免多次小幅增长导致的性能损失。
小切片的处理:
- 定义一个阈值
threshold=256
- 如果当前容量小于这个阈值,新容量直接翻倍。
大切片的处理:
- 新容量增长公式:
newcap += (newcap + 3*threshold) >> 2
- 实现了从翻倍增长到增长 1.25 倍的平滑过渡。
使用copy() 复制切片
Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()
函数的使用格式如下:
|
|
- srcSlice: 数据来源切片
- destSlice: 目标切片
|
|
从切片上删除元素
如果要删除切片中间的某一段或某一个元素,可以借助切片的截取特性,通过截取删除元素前后的切片数组,再使用append函数拼接的方式实现。这种处理方式比较优雅,并且效率很高,因为它不会申请额外的内存空间。
|
|
小结
切片是Go语言提供的重要数据类型,也是Gopher日常编码中最常使用的类型之一。切片是数组的描述符,在大多数场合替代了数组,并减少了数组指针作为函数参数的使用。
append在切片上的运用让切片类型部分支持了“零值可用”的理念,并且append对切片的动态扩容将Gopher从手工管理底层存储的工作中解放了出来。
在可以预估出元素容量的前提下,使用cap参数创建切片可以提升append的平均操作性能,减少或消除因动态扩容带来的性能损耗。
零值可用理念:
零值可用"(Zero Value Usable)是Go语言设计中的一个重要理念。这个概念意味着一个类型的零值(即变量被声明但未显式初始化时的默认值)应该是有意义且可以直接使用的。让我解释一下这个理念的核心内容和优点:
- 零值的定义:
- 在Go中,当你声明一个变量但不赋初值时,它会被自动设置为该类型的"零值"。
- 例如,int 的零值是 0,string 的零值是空字符串,指针的零值是 nil。
- 零值可用的含义:
- 这个理念要求类型的设计者考虑使零值具有实际的、合理的意义。
- 零值应该代表该类型的一个有效状态,可以安全地使用,而不会导致错误或异常行为。
- 常见的零值可用例子:
sync.Mutex
:零值就是一个未锁定的互斥锁,可以直接使用。bytes.Buffer
:零值是一个空的缓冲区,可以直接开始写入。- 切片(slice):零值是 nil,可以直接用于 append 操作。
- 优点:
- 简化初始化:减少了显式初始化的需求,使代码更简洁。
- 提高安全性:降低了因忘记初始化而导致的错误风险。
- 提升性能:避免了不必要的内存分配和初始化开销。
- 增强可读性:代码更加清晰,减少了样板代码。
- 实现零值可用的策略:
- 谨慎选择数据结构和算法,使得零值状态有意义。
- 在类型方法中正确处理零值情况。
- 如果零值不适用,考虑提供便捷的构造函数或工厂方法。