原文链接:http://oldchen.iwulai.com/index.php/2019/01/10/go%E5%9F%BA%E7%A1%80%E7%BC%96%E7%A8%8B%EF%BC%9A%E5%88%87%E7%89%87slice%EF%BC%89/
Go 语言切片(Slice)
Go 语言切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
1.定义切片
切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。
从连续内存区域生成切片是常见的操作,格式如下:
slice [开始位置:结束位置]
- slice 表示目标切片对象。
- 开始位置对应目标切片对象的索引。
- 结束位置对应目标切片的结束索引。
s[n] 切片s中索引位置为n的项
s[n:m] 从切片s的索引位置n到m-1处所获得的切片
s[n:] 从切片s的索引位置到len(s)-1处所获得的切片
s[:m] 从切片s的索引位置0到m-1处所获得的切片
s[:] 从切片s的索引位置0到len(s)-1处获得的切片
从数组生成切片,代码如下:
var a = [3]int{1, 2, 3}
fmt.Println(a, a[1:2])
//a 是一个拥有 3 个整型元素的数组,被初始化数值 1 到 3。使用 a[1:2] 可以生成一个新的切片。
代码运行结果如下:
[1 2 3] [2] //[2] 就是 a[1:2] 切片操作的结果。
从数组或切片生成新的切片拥有如下特性:
- 取出的元素数量为:结束位置-开始位置。
- 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取。
- 当缺省开始位置时,表示从连续区域开头到结束位置。
- 当缺省结束位置时,表示从开始位置到整个连续区域末尾。
- 两者同时缺省时,与切片本身等效。
- 两者同时为0时,等效于空切片,一般用于切片复位。
根据索引位置取切片 slice 元素值时,取值范围是(0~len(slice)-1),超界会报运行时错误。生成切片时,结束位置可以填写 len(slice) 但不会报错。
1) 从指定范围中生成切片
切片和数组密不可分。如果将数组理解为一栋办公楼,那么切片就是把不同的连续楼层出租给使用者。出租的过程需要选择开始楼层和结束楼层,这个过程就会生成切片。示例代码如下:
package main
import "fmt"
func main(){
var highRiseBuilding [30]int
for i := 0; i < 30; i++ {
highRiseBuilding[i] = i + 1
}
// 区间 10开始15结束
fmt.Println(highRiseBuilding[10:15])//[11 12 13 14 15]
// 中间到尾部的所有元素 20以后的所有
fmt.Println(highRiseBuilding[20:])//[21 22 23 24 25 26 27 28 29 30]
// 开头到中间的所有元素 开始到2结束
fmt.Println(highRiseBuilding[:2])//[1 2]
}
切片有点像C语言里的指针。指针可以做运算,但代价是内存操作越界。切片在指针的基础上增加了大小,约束了切片对应的内存区域,切片使用中无法对切片内部的地址和大小进行手动调整,因此切片比指针更安全、强大。
2) 表示原有的切片
生成切片的格式中,当开始和结束都范围都被忽略,则生成的切片将表示和原切片一致的切片,并且生成的切片与原切片在数据内容上是一致的,代码如下:
package main
import "fmt"
func main(){
a := []int{1, 2, 3}
fmt.Println(a[:])//[1 2 3]
}
a 是一个拥有 3 个元素的切片。将 a 切片使用 a[:] 进行操作后,得到的切片与 a 切片一致。
3) 重置切片,清空拥有的元素
把切片的开始和结束位置都设为 0 时,生成的切片将变空,代码如下:
package main
import "fmt"
func main(){
a := []int{1, 2, 3}
fmt.Println(a[0:0])//[]
}
示例:
var identifier []type//声明一个未指定大小的数组来定义切片切片不需要说明长度。
var slice1 []type = make([]type, len)//make()函数来创建切片
简写为:
slice1 := make([]type, len)
也可以指定容量,其中capacity为可选参数。
make([]T, length, capacity)
这里 len 是数组的长度并且也是切片的初始长度。
2.直接声明新的切片
除了可以从原有的数组或者切片中生成切片,你也可以声明一个新的切片。每一种类型都可以拥有其切片类型,表示多个类型元素的连续集合。因此切片类型也可以被声明。切片类型声明格式如下:
var name []T
- name 表示切片类型的变量名。
- T 表示切片类型对应的元素类型。
下面代码展示了切片声明的使用过程:
package main
import "fmt"
func main(){
// 声明字符串切片切片中拥有多个字符串。
var strList []string
// 声明整型切片切片中拥有多个整型数值。
var numList []int
// 声明一个空切片 将 numListEmpty 声明为一个整型切片。本来会在{}中填充切片的初始化元素,这里没有填充,所以切片是空的。但此时 numListEmpty 已经被分配了内存,但没有元素。
var numListEmpty = []int{}
// 输出3个切片 切片均没有任何元素,3 个切片输出元素内容均为空。
fmt.Println(strList, numList, numListEmpty)//[] [] []
// 输出3个切片大小 没有对切片进行任何操作,strList 和 numList 没有指向任何数组或者其他切片
fmt.Println(len(strList), len(numList), len(numListEmpty))//0 0 0
// 切片判定空的结果 声明但未使用的切片的默认值是 nil。strList 和 numList 也是 nil,所以和 nil 比较的结果是 true。
fmt.Println(strList == nil)//true
fmt.Println(numList == nil)//true
fmt.Println(numListEmpty == nil)//false numListEmpty 已经被分配到了内存,但没有元素,因此和 nil 比较时是 false。
}
切片是动态结构,只能与nil判定相等,不能互相判等时。
声明新的切片后,可以使用 append() 函数来添加元素。
使用 make() 函数构造切片
如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:
make( []T, size, cap )
- T:切片的元素类型。
- size:就是为这个类型分配多少个元素。
- cap:预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。
示例如下:
package main
import "fmt"
func main(){
a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b)//[0 0] [0 0]
fmt.Println(len(a), len(b))//2 2
}
a 和 b 均是预分配 2 个元素的切片,只是 b 的内部存储空间已经分配了 10 个,但实际使用了 2 个元素。
容量不会影响当前的元素个数,因此 a 和 b 取 len 都是 2。
温馨提示
使用 make() 函数生成的切片一定发生了内存分配操作。但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。
切片不一定必须经过 make() 函数才能使用。生成切片、声明后使用 append() 函数均可以正常使用切片。
3.切片初始化
s :=[] int {1,2,3 } //直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3
s := arr[:] //初始化切片s,是数组arr的引用
s := arr[startIndex:endIndex] //将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片
s := arr[startIndex:] //缺省endIndex时将表示一直到arr的最后一个元素
s := arr[:endIndex] //缺省startIndex时将表示从arr的第一个元素开始
s1 := s[startIndex:endIndex] //通过切片s初始化切片s1
s :=make([]int,len,cap) //通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片
4.切片的遍历
切片的遍历跟数组类似,两种遍历方式
package main
import "fmt"
func main(){
slice := []int{10, 20, 30, 40, 50}
//方式一、for
for i := 0; i < len(slice); i++{
fmt.Printf("原数组:%d-下标%d-值:%d\n",slice,i,slice[i])
}
fmt.Printf("-------------------------------\n")
//方式二、range
for index, val := range slice{
fmt.Printf("原数组:%d-下标%d-值:%d\n",slice,index,val)
}
}
结果:
4.修改切片元素值
切片底层还是一个数组,所以修改切片的元素可以直接通过下标索引的方式来操作。
package main
import "fmt"
func main(){
slice := []int{1, 2, 3, 100}
fmt.Printf("slice addr:%p len:%d cap:%d slice:%v\n", slice, len(slice), cap(slice), slice)
//slice addr:0xc00004c0c0 len:4 cap:4 slice:[1 2 3 100]
slice[1] = 101
fmt.Printf("slice addr:%p len:%d cap:%d slice:%v\n", slice, len(slice), cap(slice), slice)
//slice addr:0xc00004c0c0 len:4 cap:4 slice:[1 101 3 100]
}
5.切片的追加
往切片中追加元素可以使用内置的append函数,需要传入一个slice。append函数可以有两种模式
1)一次性追加多个元素。append(slice, value1, value2,..., valueN)
2)追加单个切片,并以...结尾。append(slice, append_slice...)
slice := make([]int, 2, 4)
append_slice := []int{1, 2}
fmt.Printf("slice addr:%p len:%d cap:%d slice:%v\n", slice, len(slice), cap(slice), slice)
slice = append(slice, append_slice...)
fmt.Printf("slice addr:%p len:%d cap:%d slice:%v\n", slice, len(slice), cap(slice), slice)
slice = append(slice, 10, 20)
fmt.Printf("slice addr:%p len:%d cap:%d slice:%v\n", slice, len(slice), cap(slice), slice)
slice = append(slice, 30, 40, 50)
fmt.Printf("slice addr:%p len:%d cap:%d slice:%v\n", slice, len(slice), cap(slice), slice)
//output
slice addr:0xc0420540a0 len:2 cap:4 slice:[0 0]
slice addr:0xc0420540a0 len:4 cap:4 slice:[0 0 1 2]
slice addr:0xc04207a080 len:6 cap:8 slice:[0 0 1 2 10 20]
slice addr:0xc04208a000 len:9 cap:16 slice:[0 0 1 2 10 20 30 40 50]
- 1,2可以看出当append后的length没有大于capacity时,切片指向的底层数组地址并没有改变。
- 2,3可以看出当lenght大于capacity后,切片指向的底层数组地址已经改变,并且capacity扩大了一倍。这是因为底层会重新new出一个数组,并把append后的数据全部copy过来导致的。这有点c++STL里面的vector的扩充机制。所以使用时这里也会存在一定的优化,如题能够提前预估出大小,可以减少内存拷贝的次数
这里需要明确一点的是,切片的大小改变需要通过append来扩充,而不能通过给一个大于capacity-1索引赋值来扩充,如下所示:
new_slice := make([]int, 2, 3)
new_slice[3] = 1 //runtime error: index out of range
6.切片的插入和删除
切片的增加主要还是通过append函数来完成的,总体上就是将切片拆分,拼接再组合的过程。下面分别举例说明
6.1 删除
删除下标为index的元素
slice = append(slice[:index], slice[index+1:]...)
实例如下:
package main
import "fmt"
func main(){
slice := []int{1, 3, 4, 6, 7, 9}
fmt.Printf("slice addr:%p len:%d cap:%d slice:%v\n", slice, len(slice), cap(slice), slice)
//删除下标为2的元素
slice = append(slice[:2], slice[3:]...)
fmt.Printf("slice addr:%p len:%d cap:%d slice:%v\n", slice, len(slice), cap(slice), slice)
}
输出
slice addr:0xc04200c540 len:6 cap:6 slice:[1 3 4 6 7 9]
slice addr:0xc04200c540 len:5 cap:6 slice:[1 3 6 7 9]
同理,删除index1~index2(不包含index2)之间的元素可以这样操作:
slice = append(slice[:index1], slice[index2:]...)
6.2 插入
append是在切片的尾部追加元素。有时候我们需要在切片的指定位置插入一个元素,或者一组元素。具体如下:
//在index=2的位置插入一个值为100的元素
slice := []int{1, 3, 4, 6, 7, 9}
fmt.Printf("slice addr:%p len:%d cap:%d slice:%v\n", slice, len(slice), cap(slice), slice)
//将index后面的数据保存到一个临时的切片(其实是对index后面数据的复制)
tmp := append([]int{}, slice[2:]...)
//拼接插入的元素
slice = append(slice[0:2], 100)
//与临时切片再组合得到最终的需要的切片
slice = append(slice, tmp...)
fmt.Printf("slice addr:%p len:%d cap:%d slice:%v\n", slice, len(slice), cap(slice), slice)
输出:
slice addr:0xc042072090 len:6 cap:6 slice:[1 3 4 6 7 9]
slice addr:0xc042048060 len:7 cap:12 slice:[1 3 100 4 6 7 9]
2.7切片的复制
在go语言中,一个切片赋值给另一个切片时,它们指向的底层的数组是一样的。有时候我们需要缓存切片的数据(就好比上面插入操作的第一步),就需要复制操作。前面提到了这么多,我们可以通过遍历赋值的方式也很容易实现切片的赋值。亦或者用append函数,通过往一个空切片追加当前切片的方式实现复制:
slice := []int{1,3,6}
copy_slice := append([]int{},slice[:]...)
在go语言,我们更推荐使用内置的copy函数。
func copy(dst, src []T) int
copy函数支持两个参数,第一个是目标切片,第二个是源切片,将源切片复制到目标切片上。复制的大小取dst和src中长度最小值(min(len(dst), len(src)))
package main
import "fmt"
func main(){
slice := []int{1, 3, 4, 6, 7, 9}
fmt.Printf("slice addr:%p len:%d cap:%d slice:%v\n", slice, len(slice), cap(slice), slice)
copy_slice := make([]int, 3, len(slice))
copy(copy_slice, slice)
fmt.Printf("copy_slice addr:%p len:%d cap:%d slice:%v\n", copy_slice, len(copy_slice), cap(copy_slice), copy_slice)
}
输出:
slice addr:0xc042072090 len:6 cap:6 slice:[1 3 4 6 7 9]
copy_slice addr:0xc0420720c0 len:3 cap:6 slice:[1 3 4]
如果想实现对slice的完整复制,只需要创建一个length为len(slice)大小的切片,再copy就可以了
slice := []int{1, 3, 4, 6, 7, 9}
copy_slice := make([]int, len(slice), len(slice))
copy(copy_slice, slice)
特别的当目标的length为0时,复制操作不会进行任何操作
package main
import "fmt"
func main(){
slice := []int{1, 3, 4, 6, 7, 9}
copy_slice := make([]int, 0, len(slice))
copy(copy_slice, slice)
fmt.Printf("copy_slice addr:%p len:%d cap:%d slice:%v\n", copy_slice, len(copy_slice), cap(copy_slice), copy_slice)
}
输出
copy_slice addr:0xc0420720c0 len:0 cap:6 slice:[]