概述
前言:本文主要记录了切片的定义、切片的比较、切片的拷贝、遍历,以及如何给切片追加元素。
切片的定义
由于数组长度固定、类型局限性大、无法追加元素等缺陷存在,在Go中引入了切片(slice)。
切片的底层是数组,它是一个拥有相同类型元素的可变长度的序列,支持新元素的添加,可用于快速的操作一块数据的集合。
切片是引用类型,改变切片会改变底层数组,其包含了地址
、长度
、容量
属性。切片的基本语法如下所示:
// var 关键字
//name 变量名
//T 数据类型
var name []T
//声明示例
func main() {
//声明切片的类型
var slice1 []int
var slice2 = []string{}
var slice3 = []bool{true, false}
//var slice4 = []bool{true, false}
fmt.Println(slice1)
fmt.Println(slice2)
fmt.Println(slice3)
fmt.Println(slice1 == nil)
fmt.Println(slice2 == nil)
fmt.Println(slice3 == nil)
//fmt.Println(slice3 == slice4) 会直接报错,因为切片是引用类型,不支持直接比较,只能判断是否为nil
}
输出如下所示:
[]
[]
[true false]
true
false
false
切片的长度和容量
切片的长度和容量是不一样的属性值。切片的长度指的是切片所包含的元素的个数,切片的容量是指从第一个元素开始,到切片底层数组的末尾元素的个数。
切片的长度可以用函数len()
来求,切片的容量用cap()
来求。
在make造切片中会看到两者明显的差别。
切片的表达式
切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。它有两种表达方式:一种是表达式中的start
和end
表示一个索引范围(左开右闭的区域)的简单形式,另一种是再第一种上指定容量的完整形式。
简单形式
因为切片的底层是一个数组,所以可以基于数组来得到一个切片,其长度等于end-start
的值,容量等于数组首个元素开始得到切片的底层数组的容量,如下示例:
func main() {
ar := [6]int{0, 1, 2, 3, 4, 5}
slice := ar[1:3] //slice := ar[start,end]
fmt.Printf("slice: %v, slice len is:%d ,slice cap is:%d ", slice, len(slice), cap(slice))
}
输出如下所示:
slice: [1 2], slice len is:2 ,slice cap is:5
这种简单形式可以省略掉start
和end
,如下所示:
ar[1:] //等同于 ar[1:len(ar)]
ar[:3] //等同于 ar[0:3]
ar[:] //等同于 ar[0:len(ar)]
索引越界:当0 <= start <= end <= len(ar)
的时候索引合法,否则便会索引越界。
切片再切片:对切片再进行切片表达式时,end的上界限为切片的容量cap(ar)
,而不是切片的长度len(ar)
。
索引:索引常量必须是非负数,并且可以用int
类型来表示,也必须在有效范围内,否则会在运行时panic
。
综上示例如下:
func main() {
ar := [6]int{0, 1, 2, 3, 4, 5}
slice := ar[1:3]
fmt.Printf("slice: %v, slice len is:%d ,slice cap is:%dn", slice, len(slice), cap(slice))
slice2 := slice[3:4] //索引的end是slice的cap而不是len
fmt.Printf("slice: %v, slice len is:%d ,slice cap is:%d n", slice2, len(slice2), cap(slice2))
}
输出如下所示:
slice: [1 2], slice len is:2 ,slice cap is:5
slice2: [4], slice2 len is:1 ,slice2 cap is:2
切片的完整表达式
对于数组,指向数组的指针、切片ar(不能是字符串)支持完整的切片表达式方式,如下所示:
a[start:end:max]
注意:在上述表达式中,其会构造与简单切片a[start:end]
相同类型、长度、元素的切片,其得到的切片的容量为end-start
。在这个表达式中,能省略的只有start
,省略后默认值为0
。
满足条件为0 <= start <= end <= max <=cap(ar)
如下示例:
func main() {
ar := [6]int{0, 1, 2, 3, 4, 5}
slice := ar[1:3:5]
fmt.Printf("slice: %v, slice len is:%d ,slice cap is:%dn", slice, len(slice), cap(slice))
}
输出如下:
slice: [1 2], slice len is:2 ,slice cap is:4
使用make()函数构造切片
以上的例子都是基于数组来创造切片的,当我们需要动态的创建一个切片,这时候可以用内置函数make()来创建我们需要的切片,其格式如下:
//T:表示切片的元素类型
//size:表示切片的元素数量
//cap:表示切片的容量
make([]T,size,cap)
示例:
func main() {
var slice = make([]int, 2, 5)
fmt.Println(slice)
fmt.Println(len(slice))
fmt.Println(cap(slice))
}
输出如下:
[0 0]
2
5
示例中,切片的存储空间分配了十个,但是实际上只用了两个。所以其len(slice)
才会为2,cap(slice)
表示容量为10。
切片的本质
切片的本质就是对底层数组的封装,其包含了:底层数组的指针、切片的长度、切片的容量。
//源码定义为
type slice struct {
array unsafe.Pointer
len int
cap int
}
示例:
目前有一个数组a:=[8]int{0,1,2,3,4,5,6,7}
,切片为s:=a[:4]
,其指针指向、长度和容量示意图如下:
切片s2:=a[3:7]
,相应示意图如下:
切片是否为空
检查切片是否为空,应该用len(slice)==0
来判断,不可以用slice==nil
来判断。
原因:在go中由于切片是不能进行比较的,我们不能使用==
来直接比较两个切片是否完全相同的元素。切片唯一能合法比较的为nil
,一个nil
值的切片其长度和容量都是0
,因为其没有底层数组,但是对于长度和容量都为0
的切片却不一定是nil
切片。如下所示:
var slice1 []int //len(slice1)=0,cap(slice1)=0,slice1=nil
var slice2 []int{} //len(slice2)=0,cap(slice2)=0,slice2!=nil
var slice3 = make([]int,0) //len(slice3)=0,cap(slice3)=0,slice3!=nil
slice2
与slice3
其长度和容量都为0
,但是其并不是nil
切片。
所以说判断一个切片是否为空需要用len()
函数来进行判断,而不是直接判断切片是否为空。
切片的拷贝与遍历
由于切片是引用类型,改变切片会改变底层数组。所以当多个切片共享一个底层数组的时候,改变其中一个切片中元素的值,会影响到另一个切片的内容。如下所示:
func main(){
slice1:=make([]int,3)
fmt.Println(slice1)
slice2:=slice1
slice2[2]=100
fmt.Println(slice1)
fmt.Println(slice2)
}
slice1
将值直接赋值拷贝给slice2
,所以这两个切片共享一个底层数组,改变slice2
的值,之后打印两个切片,其输出如下:
[0 0 0]
[0 0 100]
[0 0 100]
切片的遍历和数组相同,都支持for
循环的遍历,如下示例:
func main(){
s:=[]int{1,3,5}
for i:=0;i<1;i++{
fmt.Printf("index: %d, s[i]:%dn", i, s[i])
}
//for range
for index, v := range s{
fmt.Printf("index: %d, s[i]:%dn", index, v)
}
}
切片追加元素
在Go中有个内建函数append()
,可以动态的为切片追加元素。
append
函数可以动态的为一个切片一个一个的添加元素,也可以一次添加多个元素,还可以将另外一个切片元素添加进去。示例如下:
func main(){
var s:=[]int //[]
s = append(s, 1)
fmt.Printf("add one :%vn",s)
s = append(s, 2, 3, 4)
fmt.Printf("add multiple :%vn",s)
s2 := []int{10, 11, 12]
s = append(s, s2...)
fmt.Printf("add s2 :%vn",s)
}
上面代码首先定义勒一个空切片s
,第一次添加一个元素,第二次添加多个元素,第三次将另一个切片s2
的元素全部添加到s
中去。在最后一次添加的时候,需要在s2
后面添加...
,其目的是为了将切片s2
解包,类似第二次追加元素。
注意:在用append
函数的时候必须要用原来的切片接受append
的返回值。
append函数的扩容
在对切片进行追加元素的时候,如果追加元素的后切片长度超过原来切片的容量,那么会按照一定的策略对底层数组进行"扩容"操作。此时,切片指向底层数组的指针会改变,会指向另外一个能够容纳足够数量的数组。“扩容"发生在使用append
函数调用的过程中,所以我们需要用原来的切片来接收append
返回的新"切片”。
示例:
func main(){
var s []int
for i:= 0; i < 10; i++{
s = append(s,i)
fmt.Printf("s:%v, len(s):%d, cap(s):%d, iddress:%pn",s, len(s), cap(s), s)
}
}
上面代码对切片s
进行扩容,每次追加一个元素,之后分别打印出切片、切片的长度、切片的容量和切片的地址。
输出如下:
s:[0], len(s):1, cap(s):1, iddress:0xc0000a0068
s:[0 1], len(s):2, cap(s):2, iddress:0xc0000a00c0
s:[0 1 2], len(s):3, cap(s):4, iddress:0xc00009e160
s:[0 1 2 3], len(s):4, cap(s):4, iddress:0xc00009e160
s:[0 1 2 3 4], len(s):5, cap(s):8, iddress:0xc0000b60c0
s:[0 1 2 3 4 5], len(s):6, cap(s):8, iddress:0xc0000b60c0
s:[0 1 2 3 4 5 6], len(s):7, cap(s):8, iddress:0xc0000b60c0
s:[0 1 2 3 4 5 6 7], len(s):8, cap(s):8, iddress:0xc0000b60c0
s:[0 1 2 3 4 5 6 7 8], len(s):9, cap(s):16, iddress:0xc0000d0200
s:[0 1 2 3 4 5 6 7 8 9], len(s):10, cap(s):16, iddress:0xc0000d0200
从上可以看出:
append
函数将元素追加到切片的最后并返回切片;
切片容量由1、2、4、8…这种策略进行扩容。实际上在1024之前长度为之前的两倍进行扩容,之后扩容量为原来切片容量的1/4,不再以伤两倍进行增加容量,以达到满足切片需求的同时又不会造成内存浪费。
但是需要注意的是:对不同的元素类型其扩容的策略是不相同的,例如string
和int
就不相同,有兴趣的可以自己查看append()
源码。
源码路径:$GOROOT/src/runtime/slice.go
相关部分代码:
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap { //若新申请的容量大于原容量两倍,则最终的容量就是新申请的容量
newcap = cap
} else {
if old.len < 1024 { //当len不足1024之前按照两倍原容量扩容
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap { //在大于1024时新增原容量的1/4的容量
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
使用copy函数赋值切片
在Go语言中,由于切片定义为引用类型,切片a给另一个切片b赋值的时候,会指向同一块内存地址,所以改变其中一个切片元素的值,另一个切片元素对应的值也会改变。
而Go的内置函数copy()
可以迅速的将一个切片的数据复制到另一个空间中,其使用格式如下:
copy(destSlice, srcSlice []T)
destSlice:目标切片
srcSlice:数据来源切片
示例如下:
func main(){
a := []int{0, 1, 2, 3, 4}
s := make([]int, 5)
copy(s, a)
fmt.Println(a)
fmt.Println(s)
s[0] = 100
fmt.Println("After changed: ")
fmt.Println(a)
fmt.Println(s)
}
输出为:
[0 1 2 3 4]
[0 1 2 3 4]
After changed:
[0 1 2 3 4]
[100 1 2 3 4]
从切片中删除元素
Go中并没有专门的函数来删除切片元素(当然你可以自定义QAQ),我们可以根据切片的特性来"删除"其元素,自定义函数如下:
func main() {
a := []int{0, 1, 2, 3, 4, 5, 6}
fmt.Println(a)
//删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a)
}
输出如下:
[0 1 2 3 4 5 6]
[0 1 3 4 5 6]
最后
以上就是时尚小天鹅为你收集整理的Go语言之切片切片的定义切片的拷贝与遍历切片追加元素使用copy函数赋值切片从切片中删除元素的全部内容,希望文章能够帮你解决Go语言之切片切片的定义切片的拷贝与遍历切片追加元素使用copy函数赋值切片从切片中删除元素所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复