切片(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 2 3 4 5 6 7 8 9 10 11 12 13
| var s1 []int var s2 []string fmt.Println(s1, s2)
fmt.Println(s1 == nil) fmt.Println(s2 == nil)
s1 = []int{1, 2, 3} s2 = []string{"张三", " 李四", "王二"} fmt.Println(s1, s2) fmt.Println(s1 == nil) fmt.Println(s2 == nil)
|
基于数组定义切片
由于切片的底层就是一个数组,所以我们可以基于数组定义切片。
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] fmt.Println(a2) a3 := a1[:3] fmt.Println(a3) a4 := a1[6:] fmt.Println(a4) a5 := a1[:] fmt.Println(a5)
|
切片的长度和容量
切片拥有自己的长度和容量。
len() :获取切片的长度,长度就是里面元素的个数。
cap():获取切片的容量,切片的容量指底层数组从切片位置到结束的容量。
1 2 3 4
| slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} fmt.Println(len(slice)) fmt.Println(cap(slice))
|
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)) a8 := a6[5:8] fmt.Printf("len(a8):%d, cap(a8):%d", len(a8), cap(a8))
|
切片再切片
注意: 对切片进行再切片时,索引不能超过原数组的长度,否则会出现索引越界的错误。
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))
|
切片是引用传递
切片是引用层数组,当切片对底层数组进行修改会改变底层数组的值
1 2 3 4 5 6 7 8
| s3 := []int{1,3,6,9} fmt.Println(s3) s4 := s3[2:] fmt.Println(s4)
s4[1] = 100 fmt.Println(s4) fmt.Println(s3)
|
make()函数构造切片
1 2 3 4
| a := make([]int, 5, 10) fmt.Printf("v:%d, len():%d, cap():%d", a, len(a), cap(a))
|
上面代码为a的内部储存空间分配了10个,但实际上只是用了5个。容量并不会影响切片的长度。
切片的本质
切片的本质是对底层数组的封装,它包含了三个信息:底层数组的指针,切片的长度len(),和切片的长度cap()。
len() :获取切片的长度,长度就是里面元素的个数。
cap():获取切片的容量,切片的容量指底层数组从切片位置到结束的容量。
1 2
| a := [8]int{0,1,2,3,4,5,6,7} s1 := b[:5]
|


切片不能直接比较
切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部想等的元素。切片唯一合法的比较操作是和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)) fmt.Println(c1 ==nil) fmt.Printf("len:%d,cap:%d\n",len(c2), cap(c2)) fmt.Println(c2 ==nil) fmt.Printf("len:%d,cap:%d\n",len(c3), cap(c3)) fmt.Println(c3 ==nil)
|
要判断切片是否为空,是要用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) d2[1] = 5 fmt.Println(d1, d2)
|
切片遍历
切片遍历方式和数组是一样的,支持索引遍历和for-range遍历
标准for循环
1 2 3 4 5 6
| f := []int{1, 2, 3, 4, 5}
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 _, 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) 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 6
| 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)) g = append(g, 7) fmt.Printf("%d len:%d cap:%d\n", g, len(g), cap(g))
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) for i := 0; i < 10; i++ { ee = append(ee, i) } fmt.Println(ee)
|
切片的扩容策略
可以通过查看$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 { for 0 < newcap && newcap < cap { newcap += newcap / 4 } 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)。
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。
copy()函数复制切片
由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。
Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:
1 2 3
| copy(destSlice, srcSlice []T)
|
1 2 3 4 5 6 7 8
| i := []int{1, 2, 3, 4, 5} j := i k := make([]int, 5) copy(k, j) fmt.Println(i, j, k) i[1] = 6 fmt.Println(i, j, k)
|
从切片中删除元素
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) fmt.Println(l1)
|