切片解析
- Go语言切片(Slice)
-
- 一、切片的本质:为什么需要切片?
- 二、切片的声明与初始化:3种核心方式
-
- 2.1 方式1:零值切片(nil切片)——未初始化的切片
-
- 代码示例
- 关键结论:
- 2.2 方式2:基于数组创建切片——切片是数组的视图
-
- 代码示例
- 切片表达式规则:
- 注意事项:
- 2.3 方式3:make函数创建切片——直接指定长度和容量
-
- 代码示例
- 关键说明:
- 三、切片的核心操作:赋值、遍历、拷贝、删除
-
- 3.1 切片的赋值:引用传递,共享底层数组
-
- 代码示例
- 原理分析:
- 注意事项:
- 3.2 切片的遍历:2种常用方式
-
- 代码示例
- 遍历技巧:
- 3.3 切片的拷贝:copy函数实现深拷贝
-
- 代码示例
- copy函数的关键特性:
- 常见用法:
- 3.4 切片的删除:append函数实现元素删除
-
- 代码示例
- 通用删除公式:
- 注意事项:
- 四、切片的扩容机制:append函数与容量增长规则
-
- 4.1 append函数的基本用法
-
- 代码示例
- 关键特性:
- 4.2 切片的扩容规则:Go 1.18前后的差异
-
- 4.2.1 Go 1.18 之前的扩容规则
- 4.2.2 Go 1.18 之后的扩容规则
- 4.3 扩容的性能影响与优化建议
-
- 扩容的性能开销:
- 优化建议
- 五、切片的实战技巧:排序、nil切片处理、嵌套切片
-
- 5.1 切片排序:使用sort包
-
- 代码示例
- 5.2 nil切片的安全处理
-
- 代码示例
- 关键结论:
- 5.3 嵌套切片:切片的元素是切片
-
- 代码示例:
- 注意事项:
- 六、切片常见问题与避坑指南
-
- 6.1 坑点1:切片赋值后共享底层数组,修改元素导致意外修改
- 6.2 坑点2:切片扩容后,原切片与新切片指向不同底层数组
- 6.3 坑点4:使用 `for range` 遍历切片时修改元素无效
- 七、总结
Go语言切片(Slice)
在Go语言中,切片(Slice)是最常用的数据结构之一,它作为动态数组的实现,解决了数组长度固定的局限性,同时保留了数组的高效性。
一、切片的本质:为什么需要切片?
在学习切片之前,我们首先要理解数组(Array) 的局限性——Go语言中的数组是长度固定的连续内存空间,一旦声明,长度便无法修改。例如:
// 数组a的长度为5,无法动态添加或删除元素
var a [5]int = [5]int{1,2,3,4,5}
当业务场景需要动态调整数据长度时,数组就显得力不从心。而切片(Slice) 作为数组的"视图",本质是一个包含三个字段的结构体:
- 指针(Pointer):指向底层数组的起始位置
- 长度(Length):切片当前包含的元素个数(
len()
函数获取) - 容量(Capacity):切片底层数组从起始位置到末尾的元素个数(
cap()
函数获取)
这种结构既保留了数组的内存连续性(高效访问),又支持动态长度调整(动态扩容),成为Go语言中处理序列数据的首选。
二、切片的声明与初始化:3种核心方式
切片的声明和初始化是使用切片的基础,不同场景下需要选择合适的方式。以下结合代码示例详细讲解。
2.1 方式1:零值切片(nil切片)——未初始化的切片
通过 var
关键字声明切片但不初始化时,切片会处于nil状态(指针为nil,长度和容量均为0)。
代码示例
package main
import "fmt"
func main() {
// 1. 仅声明,未初始化:nil切片
var a []int
// 2. 声明并初始化空切片(非nil)
var b []int = []int{}
// 3. make创建空切片(非nil)
c := make([]int, 0)
// 判断是否为nil
if a == nil {
fmt.Println("a==nil") // 输出:a==nil
}
fmt.Println("a:", a, "len(a):", len(a), "cap(a):", cap(a)) // 输出:a: [] len(a): 0 cap(a): 0
if b == nil {
fmt.Println("b==nil") // 无输出(b非nil)
}
fmt.Println("b:", b, "len(b):", len(b), "cap(b):", cap(b)) // 输出:b: [] len(b): 0 cap(b): 0
if c == nil {
fmt.Println("c==nil ") // 无输出(c非nil)
}
fmt.Println("c:", c, "len(c):", len(c), "cap(c):", cap(c)) // 输出:c: [] len(c): 0 cap(c): 0
}
关键结论:
- nil切片:仅通过
var s []T
声明,未初始化(如示例中的a
),指针为nil,常用于表示"未赋值的空状态"(如函数返回错误时的空结果)。 - 空切片:通过
[]T{}
或make([]T, 0)
创建(如示例中的b
和c
),指针非nil,仅长度为0,常用于表示"明确的空集合"(如查询结果为空但无错误)。 - 判断切片是否为空:必须用
len(s) == 0
,而非s == nil
!因为空切片的len
也是0,但s != nil
,若用s == nil
判断会误判。
2.2 方式2:基于数组创建切片——切片是数组的视图
切片可以通过数组切片表达式(array[low:high]
)从数组中截取一部分,形成新的切片。切片的底层数组就是原数组,因此修改切片会影响原数组。
代码示例
package main
import "fmt"
func main() {
// 1. 定义一个长度为5的数组
a := [5]int{55, 56, 57, 58, 59}
// 2. 基于数组创建切片:左闭右开区间 [low, high)
b := a[1:4] // 截取数组索引1、2、3的元素(56,57,58)
fmt.Println("切片b:", b) // 输出:切片b: [56 57 58]
fmt.Println("b的长度len(b):", len(b)) // 输出:b的长度len(b): 3(元素个数)
fmt.Println("b的容量cap(b):", cap(b)) // 输出:b的容量cap(b): 4(从索引1到数组末尾共4个元素:56,57,58,59)
fmt.Printf("b的类型: %T\n", b) // 输出:b的类型: []int(切片类型)
// 3. 切片再次切片(基于切片b创建新切片c)
c := b[:len(b)] // 截取b的所有元素(等同于b[0:3])
fmt.Println("切片c:", c) // 输出:切片c: [56 57 58]
fmt.Printf("c的类型: %T\n", c) // 输出:c的类型: []int
}
切片表达式规则:
- 完整格式:
array[low:high:max]
(可选max参数,限制切片的最大容量为max - low
) - 省略规则:
low
省略时默认0(如a[:4]
等同于a[0:4]
)high
省略时默认数组长度(如a[1:]
等同于a[1:5]
)low
和high
都省略时表示整个数组(如a[:]
等同于a[0:5]
)
- 容量计算:
- 无max时:
cap(切片) = len(数组) - low
- 有max时:
cap(切片) = max - low
(避免切片越界访问原数组后续元素)
- 无max时:
注意事项:
- 切片是数组的视图,修改切片元素会同步修改原数组。例如:
b[0] = 100 // 修改切片b的第一个元素 fmt.Println(a) // 输出:[55 100 57 58 59](原数组a的索引1元素被修改)
2.3 方式3:make函数创建切片——直接指定长度和容量
make
函数是Go语言用于创建引用类型(切片、映射、通道)的内置函数,创建切片时可以直接指定长度(len) 和容量(cap),底层会自动分配一个匿名数组。
代码示例
package main
import "fmt"
func main() {
// make(类型, 长度, 容量):容量可选,默认等于长度
d := make([]int, 5, 10) // 类型int,长度5(初始5个0),容量10
fmt.Println("切片d:", d) // 输出:切片d: [0 0 0 0 0](长度5,元素默认初始化)
fmt.Println("d的长度len(d):", len(d)) // 输出:d的长度len(d): 5
fmt.Println("d的容量cap(d):", cap(d)) // 输出:d的容量cap(d): 10
fmt.Printf("d的类型: %T\n", d) // 输出:d的类型: []int
}
// 扩展示例:make创建切片后追加元素
func main6() {
// 创建长度5、容量10的字符串切片(初始5个空字符串)
var a = make([]string, 5, 10)
// 向切片追加10个元素(从索引5开始填充)
for i := 0; i < 10; i++ {
a = append(a, fmt.Sprintf("%v", i))
}
fmt.Println("最终切片a:", a)
// 输出:最终切片a: [ 0 1 2 3 4 5 6 7 8 9](前5个为空字符串,后10个为数字)
}
关键说明:
make([]T, len)
:容量默认等于长度(如make([]int, 3)
等同于make([]int, 3, 3)
)。- 切片元素的默认值:与数组一致,根据类型自动初始化(int为0,string为空串,bool为false)。
- 适用场景:明确知道切片的初始长度和预期容量时(如提前预估数据量,避免后续扩容)。
三、切片的核心操作:赋值、遍历、拷贝、删除
掌握切片的核心操作是实际开发的基础,以下结合代码示例逐一讲解。
3.1 切片的赋值:引用传递,共享底层数组
切片的赋值是引用传递,即新切片与原切片指向同一个底层数组,修改其中一个会影响另一个。
代码示例
package main
import "fmt"
func main() {
// 1. 创建切片a(长度3,容量3,底层数组[0,0,0])
a := make([]int, 3) // [0 0 0]
// 2. 切片赋值:b与a共享底层数组
b := a
// 3. 修改b的元素
b[0] = 100
// 4. 打印a和b:两者都被修改
fmt.Println("切片a:", a) // 输出:切片a: [100 0 0]
fmt.Println("切片b:", b) // 输出:切片b: [100 0 0]
}
原理分析:
- 赋值后,
a
和b
的指针、长度、容量完全相同,指向同一个底层数组。 - 若通过切片修改元素(如
b[0] = 100
),底层数组的对应位置会被修改,因此所有引用该数组的切片都会看到变化。
注意事项:
- 若需避免共享底层数组,需使用
copy
函数(见3.3节)或重新创建切片(如b := append([]int{}, a...)
)。
3.2 切片的遍历:2种常用方式
切片的遍历与数组类似,支持索引遍历和for range遍历,后者更简洁,是Go语言的推荐写法。
代码示例
package main
import "fmt"
func main() {
// 定义一个切片c
c := []int{1, 2, 3, 4, 5}
// 方式1:基于索引遍历(适合需要索引和元素的场景)
fmt.Println("=== 索引遍历 ===")
for i := 0; i < len(c); i++ {
fmt.Printf("索引%d: %d\n", i, c[i])
}
// 输出:
// 索引0: 1
// 索引1: 2
// 索引2: 3
// 索引3: 4
// 索引4: 5
// 方式2:for range遍历(适合仅需元素或同时需要索引的场景)
fmt.Println("\n=== for range遍历 ===")
for index, value := range c {
fmt.Printf("索引%d: %d\n", index, value)
}
// 输出与索引遍历一致
}
遍历技巧:
- 忽略索引:若不需要索引,可使用空白标识符
_
忽略(如for _, v := range c { ... }
)。 - 遍历部分元素:可通过切片表达式截取部分元素后遍历(如
for _, v := range c[1:3] { ... }
遍历索引1和2的元素)。 - 性能考量:for range遍历会复制元素,若切片元素是大型结构体,建议使用索引遍历或遍历指针切片(避免拷贝开销)。
3.3 切片的拷贝:copy函数实现深拷贝
由于切片赋值是引用传递,若需完全独立的切片(不共享底层数组),需使用内置的 copy
函数实现深拷贝。copy
函数的签名为:
func copy(dst, src []T) int // 返回值为实际拷贝的元素个数
代码示例
package main
import "fmt"
func main5() {
// 1. 原切片a(底层数组[1,2,3,4,5])
a := []int{1, 2, 3, 4, 5}
// 2. 创建目标切片b(长度5,容量5,底层数组[0,0,0,0,0])
b := make([]int, 5, 5)
// 3. 切片赋值:c与b共享底层数组(用于验证拷贝效果)
var c []int = b
// 4. 拷贝src(a)到dst(b):拷贝5个元素
copy(b, a)
// 5. 修改b的元素(验证是否影响a和c)
b[0] = 100
// 6. 打印结果:a未变,b和c被修改(b和c共享底层数组)
fmt.Println("原切片a:", a) // 输出:原切片a: [1 2 3 4 5](未受影响)
fmt.Println("目标切片b:", b) // 输出:目标切片b: [100 2 3 4 5](被修改)
fmt.Println("切片c:", c) // 输出:切片c: [100 2 3 4 5](与b共享底层数组)
}
copy函数的关键特性:
- 拷贝数量:取
len(dst)
和len(src)
中的较小值(如dst
长度3,src
长度5,仅拷贝3个元素)。 - 底层数组独立:拷贝后,
dst
和src
指向不同的底层数组,修改其中一个不会影响另一个。 - 浅拷贝元素:
copy
是"元素级拷贝",若切片元素是引用类型(如切片、映射),则仅拷贝指针(仍共享底层数据),需注意嵌套场景的深拷贝问题。
常见用法:
- 完全拷贝:确保
dst
的长度等于src
(如dst := make([]T, len(src))
,再copy(dst, src)
)。 - 部分拷贝:通过切片表达式指定拷贝范围(如
copy(dst, src[1:3])
拷贝src
的索引1和2的元素)。
3.4 切片的删除:append函数实现元素删除
Go语言没有专门的 delete
函数用于切片,而是通过切片表达式+append函数实现元素删除,核心思路是:将删除位置前后的元素拼接成新切片。
代码示例
package main
import "fmt"
func main() {
// 1. 定义一个字符串切片(包含4个元素)
e := []string{"北京", "上海", "广州", "深圳"}
fmt.Println("删除前:", e) // 输出:删除前: [北京 上海 广州 深圳]
// 2. 删除索引为2的元素("广州")
// 原理:将e[:2](["北京","上海"])和e[3:](["深圳"])拼接
e = append(e[:2], e[3:]...)
fmt.Println("删除后:", e) // 输出:删除后: [北京 上海 深圳]
}
通用删除公式:
删除切片 s
中索引为 index
的元素:
s = append(s[:index], s[index+1:]...)
s[:index]
:切片s
中索引0
到index-1
的元素(左半部分)。s[index+1:]...
:切片s
中索引index+1
到末尾的元素,...
表示将切片展开为可变参数。append
函数:将左半部分和右半部分拼接,返回新的切片(可能指向新的底层数组)。
注意事项:
- 索引合法性:删除前需确保
index
在[0, len(s)-1]
范围内,否则会导致切片越界(运行时panic)。 - 底层数组残留:若原切片的底层数组有其他引用,删除元素后,原位置的元素不会被立即回收(仍存在于底层数组中),可能导致内存泄漏。若需彻底清除,可手动将原位置元素置为零值(如
s[index] = nil
或s[index] = 0
)。 - 删除多个元素:可通过多次删除或切片表达式批量删除(如
s = s[:index]
删除索引index
及之后的所有元素)。
四、切片的扩容机制:append函数与容量增长规则
append
函数是切片动态扩容的核心,当切片的长度(len)等于容量(cap)时,继续追加元素会触发扩容(分配新的底层数组,拷贝原数据,更新切片的指针、长度和容量)。理解扩容规则对优化切片性能至关重要。
4.1 append函数的基本用法
append
函数的签名为:
func append(s []T, vs ...T) []T // 返回新的切片
s
:原切片。vs...
:可变参数,可传入多个元素或另一个切片(需用...
展开)。
代码示例
package main
import "fmt"
func main() {
var a []int // nil切片
// 1. 追加单个元素
a = append(a, 10)
fmt.Println("追加单个元素后:", a) // 输出:追加单个元素后: [10]
// 2. 追加多个元素
a = append(a, 11, 12, 13)
fmt.Println("追加多个元素后:", a) // 输出:追加多个元素后: [10 11 12 13]
// 3. 追加另一个切片(需用...展开)
b := []int{14, 15}
a = append(a, b...)
fmt.Println("追加切片后:", a) // 输出:追加切片后: [10 11 12 13 14 15]
}
关键特性:
append
函数不修改原切片,而是返回新的切片(原切片的指针、长度、容量可能不变)。- 若原切片容量足够(
len(s) < cap(s)
),append
会直接在底层数组的剩余空间添加元素,新切片与原切片共享底层数组。 - 若原切片容量不足(
len(s) == cap(s)
),append
会触发扩容,新切片指向新的底层数组,与原切片完全独立。
4.2 切片的扩容规则:Go 1.18前后的差异
切片的扩容规则由Go语言 runtime 源码(src/runtime/slice.go
)定义,不同版本有细微差异,以下分别讲解。
4.2.1 Go 1.18 之前的扩容规则
- 若新申请容量(cap)> 2倍旧容量(old.cap):新容量 = 新申请容量。
- 若旧切片长度(old.len)< 1024:新容量 = 2倍旧容量(翻倍扩容)。
- 若旧切片长度(old.len)≥ 1024:新容量 = 旧容量 + 旧容量/4(每次增加25%),直到新容量 ≥ 新申请容量。
- 若计算过程中出现容量溢出(新容量 ≤ 0):新容量 = 新申请容量。
package main
import "fmt"
func main() {
var a []int // nil切片(len=0, cap=0)
// 循环追加10个元素,观察容量变化
for i := 0; i < 10; i++ {
a = append(a, i)
fmt.Printf("a=%v, len=%d, cap=%d, ptr=%p\n", a, len(a), cap(a), a)
}
}
输出结果:
a=[0], len=1, cap=1, ptr=0x140000a6008
a=[0 1], len=2, cap=2, ptr=0x140000a6010
a=[0 1 2], len=3, cap=4, ptr=0x140000a8020 // len=3 <1024,cap翻倍(2→4)
a=[0 1 2 3], len=4, cap=4, ptr=0x140000a8020
a=[0 1 2 3 4], len=5, cap=8, ptr=0x140000aa040 // len=5 <1024,cap翻倍(4→8)
a=[0 1 2 3 4 5], len=6, cap=8, ptr=0x140000aa040
a=[0 1 2 3 4 5 6], len=7, cap=8, ptr=0x140000aa040
a=[0 1 2 3 4 5 6 7], len=8, cap=8, ptr=0x140000aa040
a=[0 1 2 3 4 5 6 7 8], len=9, cap=16, ptr=0x140000ac080 // len=9 <1024,cap翻倍(8→16)
a=[0 1 2 3 4 5 6 7 8 9], len=10, cap=16, ptr=0x140000ac080
4.2.2 Go 1.18 之后的扩容规则
Go 1.18 优化了大容量切片的扩容效率,调整后的规则:
- 若新申请容量(cap)> 2倍旧容量(old.cap):新容量 = 新申请容量。
- 若旧容量(old.cap)< 256:新容量 = 2倍旧容量(翻倍扩容)。
- 若旧容量(old.cap)≥ 256:新容量 = 旧容量 + (旧容量/4) + 192(每次增加25% + 192),直到新容量 ≥ 新申请容量。
- 实际分配时会根据内存对齐规则微调(可能增加少量容量,确保内存块是2的幂次倍数,提高分配效率)。
规则解读:
- 小容量切片(<256)仍保持翻倍扩容,确保高效。
- 大容量切片(≥256)改为"25% + 192"的增长方式,避免容量过大导致的内存浪费(如1024容量的切片,1.18前扩容到1280,1.18后扩容到1024+256+192=1472,更贴合实际需求)。
4.3 扩容的性能影响与优化建议
扩容的性能开销:
- 内存分配:每次扩容都需要向操作系统申请新的内存块,开销较大。
- 数据拷贝:扩容时需要将原切片的所有元素拷贝到新的底层数组,时间复杂度为O(n),元素越多,拷贝开销越大。
优化建议
- 提前预估容量:创建切片时通过
make
函数指定足够的容量(如make([]int, 0, 1000)
),避免频繁扩容。- 示例:若已知需要存储1000个int元素,直接创建
s := make([]int, 0, 1000)
,后续append无需扩容,性能提升显著。
- 示例:若已知需要存储1000个int元素,直接创建
- 避免不必要的切片创建:尽量复用切片,减少临时切片的创建(如通过
s = s[:0]
清空切片,复用底层数组)。 - 警惕切片泄漏:若切片指向大型底层数组,但仅使用少量元素,可通过
copy
函数创建小切片(如small := make([]T, len(large[:10]))
,copy(small, large[:10])
),释放原底层数组的内存。
五、切片的实战技巧:排序、nil切片处理、嵌套切片
除了基础操作,切片在实战中还有一些高频用法,以下结合代码示例讲解。
5.1 切片排序:使用sort包
Go语言的 sort
包提供了对切片的排序功能,支持int、string、float64等基本类型,核心函数包括 sort.Ints()
、sort.Strings()
、sort.Float64s()
。
代码示例
package main
import (
"fmt"
"sort"
)
func main() {
// 1. 定义一个int数组(需转换为切片后排序)
var a = [...]int{3, 7, 8, 9, 1}
fmt.Println("排序前数组a:", a) // 输出:排序前数组a: [3 7 8 9 1]
// 2. 将数组转换为切片(a[:]),调用sort.Ints排序
sort.Ints(a[:])
fmt.Println("排序后数组a:", a) // 输出:排序后数组a: [1 3 7 8 9]
}
5.2 nil切片的安全处理
nil切片虽然长度和容量为0,但可以直接用于 append
函数(无需初始化),这是Go语言的设计特性,需合理利用。
代码示例
package main
import "fmt"
func main() {
var a []int // nil切片(未初始化)
// 直接append,无需初始化(安全)
a = append(a, 1, 2, 3)
fmt.Println(a) // 输出:[1 2 3]
// 错误用法:直接对nil切片的索引赋值(会触发panic)
// a[0] = 100 // runtime error: index out of range [0] with length 0
}
关键结论:
- nil切片可以直接
append
,但不能直接通过索引赋值(需先初始化或append元素后再赋值)。 - 函数返回切片时,若结果为空且无错误,建议返回空切片(
[]T{}
或make([]T, 0)
);若结果为空且有错误,建议返回nil切片(明确表示"未赋值"状态)。
5.3 嵌套切片:切片的元素是切片
Go语言支持嵌套切片(切片的元素类型是另一个切片),常用于表示二维数据(如矩阵、表格)。
代码示例:
package main
import "fmt"
func main() {
// 1. 定义一个嵌套切片(二维int切片)
var matrix [][]int
// 2. 向嵌套切片中添加行(每行是一个切片)
matrix = append(matrix, []int{1, 2, 3})
matrix = append(matrix, []int{4, 5, 6})
matrix = append(matrix, []int{7, 8, 9})
fmt.Println("二维切片matrix:", matrix) // 输出:二维切片matrix: [[1 2 3] [4 5 6] [7 8 9]]
// 3. 遍历嵌套切片
fmt.Println("\n遍历二维切片:")
for i, row := range matrix {
for j, val := range row {
fmt.Printf("matrix[%d][%d] = %d ", i, j, val)
}
fmt.Println()
}
}
注意事项:
- 嵌套切片的每行长度可以不同(如
matrix = append(matrix, []int{10})
,形成不规则二维数据)。 - 嵌套切片的扩容仅针对外层切片,内层切片的扩容需单独处理(如
matrix[0] = append(matrix[0], 10)
为第一行追加元素)。
六、切片常见问题与避坑指南
在使用切片的过程中,容易因对底层原理理解不深而出现问题,以下总结常见坑点及解决方案。
6.1 坑点1:切片赋值后共享底层数组,修改元素导致意外修改
问题示例:
func main() {
s1 := []int{1, 2, 3}
s2 := s1 // s2与s1共享底层数组
s2[0] = 100
fmt.Println(s1) // 输出:[100 2 3](意外修改)
}
解决方案:
- 使用
copy
函数创建独立切片:s2 := make([]int, len(s1)); copy(s2, s1)
。 - 使用
append
函数创建新切片:s2 := append([]int{}, s1...)
。
6.2 坑点2:切片扩容后,原切片与新切片指向不同底层数组
问题示例:
func main() {
s1 := make([]int, 2, 2) // len=2, cap=2
s2 := s1
s1 = append(s1, 3) // s1扩容(cap变为4),指向新数组
s1[0] = 100
fmt.Println(s2) // 输出:[0 0](s2仍指向原数组,未被修改)
}
解决方案:
- 若需保持关联,提前预估容量,避免扩容(如
make([]int, 2, 3)
)。 - 若扩容后需同步修改,使用指针切片(
[]*int
),确保修改的是同一元素。
6.3 坑点4:使用 for range
遍历切片时修改元素无效
问题示例:
func main() {
s := []int{1, 2, 3}
// 错误:range遍历的是元素的副本,修改副本不影响原切片
for _, v := range s {
v *= 2
}
fmt.Println(s) // 输出:[1 2 3](未修改)
}
解决方案:
- 使用索引遍历,直接修改原切片元素:
for i := range s { s[i] *= 2 } fmt.Println(s) // 输出:[2 4 6](修改成功)
七、总结
切片作为Go语言的核心数据结构,它的设计兼顾了效率和灵活性。掌握切片的关键在于理解其底层数组+指针+长度+容量的结构,以及引用传递、扩容等特性。
最后如果哪些地方的不足,欢迎大家在评论区中指正!