切片(slice)

引子

因为数组的长度是固定不变的并且数组的长度属于类型的一部分,所以数组有很多的局限性。

1
2
3
4
5
6
7
func arraySum(arr [3]int) (int) {
var sum int
for _, a := range arr{
sum += a
}
return sum
}

该数组只能接收[3]int类型的参数,其他类型都不支持。

切片

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。

切片是一个引用类型,它的内部结构包含地址长度容量。切片一般用于快速地操作一块数据集合。

切片的定义

声明切片类型的基本语法:

1
var name []T //name为变量名,T为类型
1
2
3
4
5
6
7
8
9
10
11
12
13
//切片定义
var s1 []int // 定义一个存放int类型的切片
var s2 []string //定义一个存放string类型的切片
fmt.Println(s1, s2)
//nil类似空,没有初始化,在内存上没有开辟空间
fmt.Println(s1 == nil) //true
fmt.Println(s2 == nil) //true
//初始化
s1 = []int{1, 2, 3}
s2 = []string{"张三", " 李四", "王二"}
fmt.Println(s1, s2)
fmt.Println(s1 == nil) // false
fmt.Println(s2 == nil) // false

基于数组定义切片

由于切片的底层就是一个数组,所以我们可以基于数组定义切片。

1
2
3
4
5
6
7
8
9
10
//由数组得到切片
a1 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
a2 := a1[2:4] // 基于一个数组进行切割,从2,切到4左包含右不包含,左闭右开
fmt.Println(a2) //[3 4]
a3 := a1[:3] //从头到切片位置3
fmt.Println(a3) // [1 2 3]
a4 := a1[6:] //从切片位置6到结尾
fmt.Println(a4) //[7 8 9 0]
a5 := a1[:] //从开始切到结束
fmt.Println(a5) //[1 2 3 4 5 6 7 8 9 0]

切片的长度和容量

切片拥有自己的长度和容量。

len() :获取切片的长度,长度就是里面元素的个数。

cap():获取切片的容量,切片的容量指底层数组从切片位置到结束的容量。

1
2
3
4
//长度和容量
slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
fmt.Println(len(slice)) //10
fmt.Println(cap(slice)) //10
1
2
3
4
5
6
7
//切片的长度
a6 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
a7 := a6[0:4]
//切片的容量指底层数组从切片位置到结束的容量
fmt.Printf("len(a7):%d, cap(a7):%d", len(a7), cap(a7)) //len(a7):4, cap(a7):10
a8 := a6[5:8]
fmt.Printf("len(a8):%d, cap(a8):%d", len(a8), cap(a8)) //len(a7):3, cap(a7):5

切片再切片

注意: 对切片进行再切片时,索引不能超过原数组的长度,否则会出现索引越界的错误。

1
2
3
4
5
6
//切片再切片
//注意: 对切片进行再切片时,索引不能超过原数组的长度,否则会出现索引越界的错误。
a9 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
a10 := a9[5:]
a11 := a10[1:2]
fmt.Printf("len(a11):%d, cap(a11):%d", len(a11), cap(a11)) //len(a11):1, cap(a11):4

切片是引用传递

切片是引用层数组,当切片对底层数组进行修改会改变底层数组的值

1
2
3
4
5
6
7
8
s3 := []int{1,3,6,9}
fmt.Println(s3)
s4 := s3[2:]
fmt.Println(s4) // [6 9]
//切片是引用层数组,当切片对底层数组进行修改会改变底层数组的值
s4[1] = 100
fmt.Println(s4) //[6 100]
fmt.Println(s3) ////[1 3 6 100]

make()函数构造切片

1
2
3
4
make([]T, len, cap)
//T:切片元素的类型
//len:切片的长度
//cap:切片的容量
1
2
3
4
//使用make()函数构造切片
a := make([]int, 5, 10) //[0 0 0 0 0] 长度都会默认零值,并占用占空间
fmt.Printf("v:%d, len():%d, cap():%d", a, len(a), cap(a))
//v:[0 0 0 0 0], len():5, cap():10

上面代码为a的内部储存空间分配了10个,但实际上只是用了5个。容量并不会影响切片的长度。

切片的本质

切片的本质是对底层数组的封装,它包含了三个信息:底层数组的指针,切片的长度len(),和切片的长度cap()。

len() :获取切片的长度,长度就是里面元素的个数。

cap():获取切片的容量,切片的容量指底层数组从切片位置到结束的容量。

1
2
a := [8]int{0,1,2,3,4,5,6,7}
s1 := b[:5]

slice_01

1
s2 := a[3:6]

slice_01

切片不能直接比较

切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部想等的元素。切片唯一合法的比较操作是和nil比较。一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但我们不能说一个长度为0的切片一定是nil.

1
2
3
4
5
6
7
8
9
var c1 []int
c2 := []int{}
c3 := make([]int, 0)
fmt.Printf("len:%d,cap:%d\n",len(c1), cap(c1)) //len:0,cap:0
fmt.Println(c1 ==nil) //true
fmt.Printf("len:%d,cap:%d\n",len(c2), cap(c2)) //len:0,cap:0
fmt.Println(c2 ==nil) //false
fmt.Printf("len:%d,cap:%d\n",len(c3), cap(c3)) //len:0,cap:0
fmt.Println(c3 ==nil) //false

要判断切片是否为空,是要用len(s) == 0来判断,不应使用s == nil来判断。

切片的赋值拷贝

切片是引用类型,对切片进行修改会影响另一个切片的内容

1
2
3
4
5
6
7
//切片的拷贝和和赋值
//切片是引用类型,对切片进行修改会影响另一个切片的内容
d1 := []int{1, 2, 3, 4}
d2 := d1[2:]
fmt.Println(d1, d2) //[1 2 3 4] [3 4]
d2[1] = 5
fmt.Println(d1, d2) //[1 2 3 5] [3 5]

切片遍历

切片遍历方式和数组是一样的,支持索引遍历和for-range遍历

标准for循环

1
2
3
4
5
6
//切片遍历
f := []int{1, 2, 3, 4, 5}
//标注for循环
for i := 0; i < len(f); i++ {
fmt.Println(f[i])
}

for-range

1
2
3
4
5
f := []int{1, 2, 3, 4, 5}
//for-range
for _, v := range f{
fmt.Println(v)
}

append()方法为切片添加元素

Go语言的内建函数append()可以为切片动态添加元素,每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。

append()追加元素到切片最后并会返回该切片。

注意:切片扩容后指向的底层数组就会更换。

1
2
3
4
cc := []int{1,2,3}
fmt.Printf("%d len:%d cap:%d ptr:%p\n", cc, len(cc), cap(cc), cc) //[1 2 3] len:3 cap:3 ptr:0xc00000e420
cc = append(cc,4,5)
fmt.Printf("%d len:%d cap:%d ptr:%p\n", cc, len(cc), cap(cc), cc) //[1 2 3 4 5] len:5 cap:6 ptr:0xc00000c3f0
1
2
3
4
5
6
//append(),为切片追加元素
var sumSlice []int
for i := 0; i < 10; i++ {
sumSlice = append(sumSlice, i)
fmt.Printf("%d len:%d cap:%d ptr:%p\n", sumSlice, len(sumSlice), cap(sumSlice), sumSlice)
}

输出:

1
2
3
4
5
6
7
8
9
10
[0] len:1 cap:1 ptr:0xc00000a358
[0 1] len:2 cap:2 ptr:0xc00000a380
[0 1 2] len:3 cap:4 ptr:0xc00000e3e0
[0 1 2 3] len:4 cap:4 ptr:0xc00000e3e0
[0 1 2 3 4] len:5 cap:8 ptr:0xc000010340
[0 1 2 3 4 5] len:6 cap:8 ptr:0xc000010340
[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc000010340
[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc000010340
[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc00008c080
[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc00008c080

append()支持一次追加多个元素。

...表示拆开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
g := []int{1, 3, 5}
//追加一个元素
fmt.Printf("%d len:%d cap:%d\n", g, len(g), cap(g)) //[1 3 5] len:3 cap:3
g = append(g, 7)
fmt.Printf("%d len:%d cap:%d\n", g, len(g), cap(g)) //[1 3 5 7] len:4 cap:6

//同时追加多个元素
h := []string{"北京", "深圳"}
h = append(h, "南充", "西安")
fmt.Println(h) //[北京 深圳 南充 西安]

//通过切片形式追加
str := []string{"成都", "重庆"}
h = append(h, str...) //...表示拆开
fmt.Println(h) //[北京 深圳 南充 西安 成都 重庆]
1
2
3
4
5
ee := make([]int, 5, 10) //创建出来的切片是 [0 0 0 0 0]
for i := 0; i < 10; i++ {
ee = append(ee, i)
}
fmt.Println(ee) //[0 0 0 0 0 0 1 2 3 4 5 6 7 8 9]

切片的扩容策略

可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}

从上面的代码可以看出以下内容:

  • 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
  • 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
  • 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
  • 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如intstring类型的处理方式就不一样。

copy()函数复制切片

由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:

1
2
3
copy(destSlice, srcSlice []T)
//destSlice:目标切片,需要提前指定切片大小,比数据源切片大
//srcSlice: 数据切片来源
1
2
3
4
5
6
7
8
//copy切片的复制
i := []int{1, 2, 3, 4, 5}
j := i
k := make([]int, 5)//目标切片,需要提前指定切片大小,比数据源切片大
copy(k, j)
fmt.Println(i, j, k) //[1 2 3 4 5] [1 2 3 4 5] [1 2 3 4 5]
i[1] = 6
fmt.Println(i, j, k)//[1 6 3 4 5] [1 6 3 4 5] [1 2 3 4 5]

从切片中删除元素

Go语言中没有专门删除切片的方法,但是可以使用切片本身的特性来删除元素。

没删除的位置整体前移,空下来的位置还是原来的值

1
2
3
4
5
l := [...]int{1,2,3,4,5,6,7}
l1 := l[:]
l1 = append(l1[:2], l1[4:]...)
fmt.Println(l) //[1 2 5 6 7 6 7]
fmt.Println(l1) // [1 2 5 6 7]