切片(slice)
切片的底层是数组实现的,可以按需自动增长和缩小。切片是数组的引用,因此是引用类型,不支持直接比较,只能和nil比较。切片的动态增长是通过内建函数append()来实现的,这个函数可以快速且高效地增长切片,也可以通过切片再次切割,缩小每一个切片的大小。
- 切片不存值,底层数组存值
- 切片指向一个底层数组
- 底层数组是占用一块连续的内存空间
创建数组切片
创建两个类型分别为 int 型和 string 型的切片,并初始化
func main(){
var slice1 []int
var slice2 []string
fmt.Println(slice1,slice2)
fmt.Println(slice1 == nil) //true,没有开辟内存空间
fmt.Println(slice2 == nil) //true
//初始化
slice1 = []int{1,3,5}
slice2 = []string{"小","马","锅"}
fmt.Println(slice1,slice2)
fmt.Println(slice1 == nil) //false,初始化已分配内存空间
fmt.Println(slice2 == nil) //false
}
/*
[] []
true
true
[1 3 5] [小 马 锅]
false
false
*/
可以看到,未初始化时的切片为 [ ],即切片的零值为 [ ]。由于切片是引用类型,切片与切片之间不能直接比较,只能与nil比较。未初始化的 slice1 和 slice2 由于没有分配到内存空间,因此与nil比较的值为 true ,初始化的 slice1 和 slice2 已经分配了内存空间,因此与 nil 比较的值为 false (分配的内存空间不同)。
- make()函数创建切片
func main(){
//创建长度5,容量10的切片,未被初始化
s1 := make([]int, 5,10)
fmt.Println(s1)
fmt.Printf("s1长度:%d s1容量:%d\n",len(s1),cap(s1))
//创建长度0,容量10的切片,空切片
s2 := make([]int, 0 , 10)
fmt.Println(s2)
fmt.Printf("s2长度:%d s2容量:%d\n",len(s2),cap(s2))
}
/*
[0 0 0 0 0]
s1长度:5 s1容量:10
[]
s2长度:0 s2容量:10
*/
- 使用切片字面量创建切片
func main(){
s1 := []int{1,2,3,5,4,20}
fmt.Println(s1)
fmt.Printf("s1长度:%d s1容量:%d\n",len(s1),cap(s1))
s2 := []string{"字","面","量"}
fmt.Println(s2)
fmt.Printf("s2长度:%d s2容量:%d\n",len(s2),cap(s2))
}
/*
[1 2 3 5 4 20]
s1长度:6 s1容量:6
[字 面 量]
s2长度:3 s2容量:3
*/
nil 切片和空切片
有时候需要声明一个值为 nil 的切片,也叫空切片;nil 切片在底层数组中包含 0 个元素,也没有分配任何的存储空间。nil 切片还可以用来表示空集合。一个 nil 切片没有底层数组,它的长度和容量都是 0 以下是创建空切片的三种方式:声明未初始化的切片、使用make函数,使用切片字面量。
func main(){
//声明未初始化的切片是空切片,值为 nil
var slice []int
fmt.Println(slice)
//使用make函数创建空的字符型切片
s1 := make([]string, 0)
fmt.Println(s1)
//使用字面量创建空的布尔型切片
s2 := []bool{}
fmt.Println(s2)
}
/*
[]
[]
[]
*/
长度和容量都为 0 的切片不一定都是 nil 切片(空切片),判断一个切片是否为空,应该使用 len(slice) == 0,不能使用 slice == nil 判断。
切片的长度和容量
切片拥有自己的长度和容量,可以通过使用内建函数 len() 求长度,cap() 求容量。
func main(){
var slice1 []int
var slice2 []string
//初始化
slice1 = []int{1,3,5,9,65,3}
slice2 = []string{"小","马","锅"}
//长度和容量
fmt.Printf("slice1长度:%d slice1容量:%d\n",len(slice1),cap(slice1))
fmt.Printf("slice2长度:%d slice2容量:%d\n",len(slice2),cap(slice2))
}
/*
slice1长度:6 slice1容量:6
slice2长度:3 slice2容量:3
*/
由数组得到切片
func main(){
array := [...]int{2,3,5,1,8,13,31,9}
//切片是基于底层数组的切片
s := array[0:4] //从第0个到第4个的切片(不包含最后一个) -> [2 3 5 1]
s1 := array[1:6] //从第1个到第6个的切片(不包含最后一个) ->[3 5 1 8 13]
s2 := array[:] //全切(包含所有元素) -> [2 3 5 1 8 13 31 9]
s3 := array[:5] //从第0个到第5个(不包含最后一个) -> [2 3 5 1 8]
s4 := array[4:] //从第4个到结束(包括最后一个) -> [8 13 31 9]
fmt.Printf("array:%d\n",array)
fmt.Printf("s:%d\ns1:%d\ns2:%d\ns3:%d\ns4:%d\n",s,s1,s2,s3,s4)
}
/*
array:[2 3 5 1 8 13 31 9]
s:[2 3 5 1]
s1:[3 5 1 8 13]
s2:[2 3 5 1 8 13 31 9]
s3:[2 3 5 1 8]
s4:[8 13 31 9]
*/
切片的长度和容量
func main(){
array := [...]int{2,3,5,1,8,13,31,9}
// s := array[0:4] //从第0个到第4个的切片(不包含最后一个) -> [2 3 5 1]
s1 := array[1:6] //从第1个到第6个的切片(不包含最后一个) ->[3 5 1 8 13]
s2 := array[:] //全切(包含所有元素) -> [2 3 5 1 8 13 31 9]
s3 := array[:5] //从第0个到第5个(不包含最后一个) -> [2 3 5 1 8]
s4 := array[3:6] //从第3个到第6个(不包含最后一个)-> [1 8 13]
//切片长度和容量与数组的关系
fmt.Printf("array:%d\n",array)
fmt.Printf("array长度:%d array容量:%d\n",len(array),cap(array))
fmt.Printf("s1长度:%d s1容量:%d\n",len(s1),cap(s1))
fmt.Printf("s2长度:%d s2容量:%d\n",len(s2),cap(s2))
fmt.Printf("s3长度:%d s3容量:%d\n",len(s3),cap(s3))
fmt.Printf("s4长度:%d s4容量:%d\n",len(s4),cap(s4))
}
/*
array:[2 3 5 1 8 13 31 9]
array长度:8 array容量:8
s1长度:5 s1容量:7
s2长度:8 s2容量:8
s3长度:5 s3容量:8
s4长度:3 s4容量:5
*/
- 切片是引用类型,实际就是指向底层数组的指针
- 切片的长度就是它的元素个数
- 切片容量就是底层数组从切片第一个元素到最后一个元素的长度
由切片得到切片
func main(){
array := [...]int{2,3,5,1,8,13,31,9}
// s := array[0:4] //从第0个到第4个的切片(不包含最后一个) -> [2 3 5 1]
s1 := array[1:6] //从第1个到第6个的切片(不包含最后一个) ->[3 5 1 8 13]
s2 := s1[1:3] //从s1切片第1个到第3个元素(不包含最后一个) -> [5 1]
//切片长度和容量与数组的关系
fmt.Printf("array:%d\n",array)
fmt.Printf("array长度:%d array容量:%d\n",len(array),cap(array))
fmt.Printf("s1长度:%d s1容量:%d\n",len(s1),cap(s1))
//由切片组成切片
fmt.Printf("s2:%d\n",s2)
fmt.Printf("s2长度:%d s2容量:%d\n",len(s2),cap(s2))
//切片是引用类型,如果改变底层数组,切片也会改变
array[3] = 8888
fmt.Printf("array修改第3个元素:%d\n",array)
fmt.Printf("s1修改:%d\ns2修改:%d\n",s1,s2)
}
/*
array:[2 3 5 1 8 13 31 9]
array长度:8 array容量:8
s1长度:5 s1容量:7
s2:[5 1]
s2长度:2 s2容量:6
array修改第3个元素:[2 3 5 8888 8 13 31 9]
s1修改:[3 5 8888 8 13]
s2修改:[5 8888]
*/
可以看到,切片 s2 是由切片 s1 从第1个元素切到第3个元素(不包括最后一个)而形成的切片,为 [5 1] 。因为切片是对底层数组的引用,又称引用类型;因此,改变底层数组的值,切片也会改变。但是这里可以分为两种情况:
例如,使用 [ ] 修改底层数组 array 索引值3的元素为 8888 ,又因为 8888 分别是切片 s1 和 s2 对应索引值 2 和 1 的值,因此切片会发生改变;如果修改底层数组 array 索引值0的元素为 8888,又因为 8888 不在切片 s1 和 s2 的范围内,所以切片不会受到底层数组的改变而影响。
- 若改变底层数组的索引值对应的元素,不在对应切片内的元素,则切片不会发生改变(情况一)
//情况一 · 修改底层数组元素不在切片范围内
func main(){
array := [...]int{2,3,5,1,8,13,31,9}
s1 := array[1:6] //从第1个到第6个的切片(不包含最后一个) ->[3 5 1 8 13]
s2 := s1[1:3] //从s1切片第1个到第3个元素(不包含最后一个) -> [5 1]
//由切片组成切片
fmt.Printf("s2:%d\n",s2)
fmt.Printf("s2长度:%d s2容量:%d\n",len(s2),cap(s2))
//切片是引用类型,如果改变底层数组,切片也会改变
array[0] = 8888
fmt.Printf("array修改第0个元素:%d\n",array)
fmt.Printf("s1:%d\ns2:%d\n",s1,s2)
}
/*
s2:[5 1]
s2长度:2 s2容量:6
array修改第0个元素:[8888 3 5 1 8 13 31 9]
s1:[3 5 1 8 13]
s2:[5 1]
*/
- 若改变底层数组的索引值对应的元素,是在对应切片内的元素,则切片也会发生改变(情况二)
////情况二 · 修改底层数组元素在切片范围内
func main(){
array := [...]int{2,3,5,1,8,13,31,9}
s1 := array[1:6] //从第1个到第6个的切片(不包含最后一个) ->[3 5 1 8 13]
s2 := s1[1:3] //从s1切片第1个到第3个元素(不包含最后一个) -> [5 1]
//由切片组成切片
fmt.Printf("s2:%d\n",s2)
fmt.Printf("s2长度:%d s2容量:%d\n",len(s2),cap(s2))
//切片是引用类型,如果改变底层数组,切片也会改变
//情况二
array[3] = 8888
fmt.Printf("array修改第3个元素:%d\n",array)
fmt.Printf("s1:%d\ns2:%d\n",s1,s2)
}
/*
s2:[5 1]
s2长度:2 s2容量:6
array修改第3个元素:[2 3 5 8888 8 13 31 9]
s1:[3 5 8888 8 13]
s2:[5 8888]
*/
切片的赋值
func main() {
s1 := []string{"red", "yellow", "blue", "green"}
fmt.Printf("s1:%s\n",s1)
//修改第0个元素的值
s1[0] = "black"
s2 := s1 //s2和s1都指向同一个底层数组
fmt.Printf("s1:%s\ns2:%s\n",s1,s2)
}
/*
s1:[red yellow blue green]
s1:[black yellow blue green]
s2:[black yellow blue green]
*/
切片遍历
- 索引遍历
- range遍历,总是从切片的头部(索引值为0的元素)开始的
func main(){
array := []int{1,2,3}
//按索引遍历
for i:=0; i<len(array) ; i++ {
fmt.Println(array[i])
}
//range遍历
for i,v := range array {
fmt.Println(i,v)
}
}
/*
1
2
3
0 1
1 2
2 3
*/
切片扩容
-
使用append()函数进行切片扩容
- 调用append函数必须用原来的切片变量接收返回值
- 必须用变量接收函数返回值
- 若底层数组不够,append函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值
- 切片扩容会根据切片类型不同而做不同的处理,比如 int 和 string 类型的处理方式是不一样的
func main(){
//原始切片
slice := []int{1,2,3}
fmt.Printf("slice:%d len(slice):%d cap(slice):%d\n",slice,len(slice),cap(slice))
//第一次切片扩容,观察长度和容量
s1 := append(slice,4,5)
fmt.Printf("s1:%d len(s1):%d cap(s1):%d\n",s1,len(s1),cap(s1))
//第二次切片扩容,观察长度和容量
s1 = append(s1,6,7,8)
fmt.Printf("s1:%d len(s1):%d cap(s1):%d\n",s1,len(s1),cap(s1))
//第三次切片扩容,观察长度和容量
s1 = append(s1,9,10,11,12,15)
fmt.Printf("s1:%d len(s1):%d cap(s1):%d\n",s1,len(s1),cap(s1))
//追加原始切片slice元素 ... 代表拆分切片内元素,比如[1][2][3]
s1 = append(s1, slice...)
fmt.Printf("s1:%d len(s1):%d cap(s1):%d\n",s1,len(s1),cap(s1))
}
/*
slice:[1 2 3] len(slice):3 cap(slice):3
s1:[1 2 3 4 5] len(s1):5 cap(s1):6
s1:[1 2 3 4 5 6 7 8] len(s1):8 cap(s1):12
s1:[1 2 3 4 5 6 7 8 9 10 11 12 15] len(s1):13 cap(s1):24
s1:[1 2 3 4 5 6 7 8 9 10 11 12 15 1 2 3] len(s1):16 cap(s1):24
*/
函数append()会智能地处理底层数组的容量。可以看出,在创建新的底层数组时,与原始底层数组相比较,新数组容量是原始数组容量的两倍。在切片容量小于 1024 个元素时,总是会成倍地增长容量。一旦元素超过 1024 个,容量的增长因子将会设置为 1.25 ,也就是每次会增加 25% 的容量。【注】:关于切片容量增长的源代码在 $GOROOT/src/runtime/slice.go
切片复制
使用copy()函数进行切片复制
func main(){
a1 := []int{1,3,5}
a2 := a1
var a3 = make([]int,3,3)
copy(a3,a1) //把切片a1复制给a3(副本)
fmt.Println(a1,a2,a3)
a1[0] = 100 //修改a1索引值0的元素
fmt.Println(a1,a2,a3) //a2共用a1的底层数组会改变,副本a3不受影响
}
/*
[1 3 5] [1 3 5] [1 3 5]
[100 3 5] [100 3 5] [1 3 5]
*/
切片删除
Go语言中没有删除切片的专用方法,但是可以利用切片的特性进行删除操作。
func main(){
array := [...]int{1,2,3,4,5,6,7} //底层数组
slice := array[:] //全切,把底层数组转化成切片类型
//删除索引值2的元素,即删除3
fmt.Printf("原始array容量:%d 原始slice容量:%d\n",cap(array),cap(slice))
//先把[1 2]和[4 5 6 7]切开,再拼接
slice = append(slice[:2],slice[3:]...)
fmt.Println(slice)
fmt.Printf("修改slice容量:%d\n",cap(slice)) //切片不存值,底层数组存,删除切片元素等价于元素向左移,内存空间没被删,容量也就不变
}
/*
原始array容量:7 原始slice容量:7
[1 2 4 5 6 7]
修改slice容量:7
*/
切片不存值,底层数组存值,删除切片元素可以看作是切片元素范围移动,而数组本身所占用的内存空间没有被删,因此原始切片容量和删除切片后容量是一致的,都是7
func main(){
array := []int{1,2,3,4,6,8,5,13,25}
slice := array[:]
fmt.Printf("原始数组array:%d\n",array)
//删除索引值为4的元素6
slice = append(slice[:4],slice[5:]...)
fmt.Printf("修改切片slice:%d\n",slice)
fmt.Printf("修改数组array:%d\n",array)
}
/*
原始数组array:[1 2 3 4 6 8 5 13 25]
修改切片slice:[1 2 3 4 8 5 13 25]
修改数组array:[1 2 3 4 8 5 13 25 25]
*/
这里原始数组和修改数组不同的原因是:当删除索引值为4的元素6时,就相当于把 slice [ 5 : ] 这部分的切片元素都追加到 slice [ : 4 ] 后面,此时索引值是4的切片元素6已经被删除,而底层数组本身的内存空间是不改变的,因此在使用 append() 函数追加以后,底层数组最后一位元素是原始数组的最后一位元素,即 25 。
练习(demo)
一道考察切片基础的面试题
func main(){
//未初始化时的切片是零值
var slice = make([]int,5,10)
fmt.Println(slice)
//在int型切片零值的基础上追加元素
for i:=0 ; i<10 ; i++ {
slice = append(slice,i)
}
fmt.Println(slice)
}
/*
[0 0 0 0 0]
[0 0 0 0 0 0 1 2 3 4 5 6 7 8 9]
*/
有疑问加站长微信联系(非本文作者)