[golang]语法基础之切片

说明

切片是go语言当中的一种数据结构,整体来说,与前面说到的数组非常类似,但是相对于数组来说切片更加的灵活,原因在于切片的设计主要围绕着动态数组的概念设计的。可以根据需要自动更改大小,使用切片这种数据结构,可以比数组更加方便的管理和使用数据集合。

内部实现

从本质来说,切片是基于数组实现的,底层是数组,且切片本身非常小,可以理解为对底层的数组的抽象。也正是因为这样的原因,切片的底层和数组一样同样是连续匹配的,所以切片的效率也同样是极高的。同时和数组相同可以通过索引值获得数据,也可以进行迭代等操作。

切片对象之所以小的原因是因为它是一个只有三个字段的数据结构:

  • 一个是指向底层数组的指针
  • 一个是切片的长度
  • 一个是切片的容量

这三个字段,就是go语言操作底层数组的元数据,通过这三个字段,我们可以任意的操作切片。

声明和初始化

在go语言当中,切片的创建方式有多种,我们逐一的来说。

首先,来说最简单的声明方式,通过make方式来创建。

slice := make([]int,5)

使用内置的make函数时,需要传入一个参数,指定切片的长度,上述代码中的数字5表示的就是切片的长度,需要知道的是,此时切片的容量也是5,在go语言当中也允许单独指定切片的容量。

s1 := make([]int , 5 , 10 )

上述代码中,切片的长度是5,容量是10(这个容量10对应的是切片底层数组的)。

上面我们说到切片的本质其实是一个数组,所以我们在创建切片的时候,如果没有指定字面值的话,默认值就是数组元素的零值。

上面的代码中,我们设置了容量是10,但是我们实际只能访问5个元素,因为切片的长度被设置为5,如果想要访问剩下的元素,需要先进行扩充。切片扩充之后才可以访问。

需要注意的是容量必须>=长度,不予许创建长度大于容量的切片。

还有一种创建切片的方式,是通过字面量,就是指定初始化的值。

s1 :=[]int{1,2,3,4,5}

上面的这种创建方式和数组非常类似,但是不需要定义[]当中的值,上面的这种写法切片的长度和容量都是相等,会根据我们指定的字面量推导出来。

我们也可以像数组那样在切片创建的时候初始化切片某个索引的值:

slice:=[]int{4:1}

上面的代码中指定了第五个元素为1,其他元素都是默认值0.这时候切片的长度和容量也是一样的。

一定要注意,创建数组时必须要设置长度或者设置为...

//数组
array:=[5]int{4:1}
//切片
slice:=[]int{4:1}

切片还有nil切片和空切片,它们的长度和容量都是0,但是它们指向底层数组的指针不一样,nil切片意味着指向底层数组的指针为nil,而空切片对应的指针是个地址。

如下:

//nil切片
var nilSlice []int
//空切片
slice:=[]int{}

一般来说,nil切片表示不存在的切片,而空切片表示一个空集合,它们各有用处。

切片另外一个用处比较多的创建是基于现有的数组或者切片创建。

slice := []int{1, 2, 3, 4, 5}
slice1 := slice[:]
slice2 := slice[0:]
slice3 := slice[:5]

fmt.Println(slice1)
fmt.Println(slice2)
fmt.Println(slice3)

基于现有的切片或者数组创建新的切片,使用[i:j]这样的操作符即可。表示从i索引值开始,到j索引值结束,截取原数组或者切片,创建而成的新切片,新切片的值包含原切片的i索引,但是不包含j索引。

i如果省略,默认是0;j如果省略默认是原数组或者切片的长度,所以上述代码中的三个切片的值是一样的。需要知道的是,这里的i和j都不能超过原切片或者数组的索引。

slice := []int{1, 2, 3, 4, 5}
newSlice := slice[1:3]

newSlice[0] = 10
    
fmt.Println(slice) // [1 10 3 4 5]
fmt.Println(newSlice) // [10 3]

我们可以通过上述的代码看到,新的切片和原切片共用的是一个底层数组,所以当修改的时候,底层数组的值会被改变,所以原切片的值也就被改变了。同理,对于基于数组的切片也是一样的。

如果基于原数组或者切片创建一个新的切片后,那么新的切片的大小和容量是多少呢?

可以参照下面的公式:

对于底层数组容量是k的切片slice[i:j]来说
长度:j-i
容量:k-i

比如上面的例子当中,slice[1:3] ,长度是3-1=2,容量是5-1=4。看上去有些麻烦对吗?

如果在代码中想要获取长度和容量,go语言给我们提供了len()函数用来计算切片的长度,提供了cap()函数用来计算切片的容量。

slice := []int{1,2,3,4,5}
s := slice[1:3]
fmt.Printf("切片s的长度是%d,容量是%d \n",len(s),cap(s))

以上是基于一个数组或者切片使用两个索引创建新切片的方法,除此之外还有一种通过三个索引值创建切片的方法,第三个用来限定新切片的容量。

语法格式为:

slice[i:j:k]

例如:

slice := []int{1, 2, 3, 4, 5}
newSlice := slice[1:2:3]

在上面的代码中,我们创建了一个长度为2-1=1,容量为3-1=2的新切片,不过需要注意的是,第三个索引不能超过原切片的最大索引值5。

使用切片

去使用一个切片,和使用数组一样,都是通过索引值就可以获取切片相对应的元素的值,同样也可以修改对应元素的值。

例如:

s1 := []int{1,2,3,4,5}
fmt.Println(s1[2]) // 获取值  3 
s1[2] = 10 // 修改值
fmt.Println(s1[2]) // 输出10 

切片只能访问到其长度内的元素,访问超过长度外的元素,会导致运行时异常,与切片容量关联的元素只能用于切片增长。

在上面我们说过,切片是一个动态的数组,可以实现按需增长,我们可以使用go内置的append函数实现切片的增长。

append函数可以为切片追加一个元素。

例如:

s1 := []int{1,2,3,4,5}
s2 := s1[0:2]

// 通过append方法想s2中追加
s2 = append(s2,9)
fmt.Println(s2) // [1 2 9]
fmt.Println(s1) // [1 2 9 4 5]

在上面的代码中,我们通过append函数为新创建的切片s2追加了一个元素9,当我们通过打印后可以发现,原切片s1索引值为2的位置上的值也被改变了,变成了9。

引起这种结果的原因是因为newSlice有可用的容量,不会创建新的切片来满足追加,所以直接在newSlice后追加了一个元素10,因为newSlice和slice切片共用一个底层数组,所以切片slice的对应的元素值也被改变了。

这里newSlice新追加的第3个元素,其实对应的是slice的第4个元素,所以这里的追加其实是把底层数组的第4个元素修改为10,然后把newSlice长度调整为3。

需要知道的是,如果当切片的底层数组,没有足够的容量时,就会新建一个底层数组,把原来数组的值复制到新底层数组里,再追加新值,这时候就不会影响原来的底层数组了。

所以一般我们在创建新切片的时候,最好要让新切片的长度和容量一样,这样我们在追加操作的时候就会生成新的底层数组,和原有数组分离,就不会因为共用底层数组而引起奇怪问题,因为共用数组的时候修改内容,会影响多个切片。

append函数会智能的增长底层数组的容量,目前的算法是:容量小于1000个时,总是成倍的增长,一旦容量超过1000个,增长因子设为1.25,也就是说每次会增加25%的容量。

内置的append函数也是一个可变参数的函数,所以我们可以同时追加几个值。

s2 = append(s2,10,20,30)

此外,我们还可以通过...操作符,把一个切片追加到另外一个切片里。

s1 := []int{1,2,3,4,5}
s2 := s1[1:2:3]

s2 = append(s2,s1...)
fmt.Println(s2)
fmt.Println(s1)

迭代切片

既然切片是一个集合,我们可以使用for range 循环来迭代它,打印其中的每个元素以及对应的索引。

slice := []int{1, 2, 3, 4, 5}
for i,v:=range slice{
    fmt.Printf("索引:%d,值:%d\n",i,v)
}

如果我们不想要索引,可以使用_来忽略它,这时go的用法,很多不需要的函数返回值,都可以忽略。

 slice := []int{1, 2, 3, 4, 5}
for _,v:=range slice{
    fmt.Printf("值:%d\n",v)
}

这里需要说明的是range返回的是切片元素的复制,而不是元素的引用

除了for range循环外,我们也可以使用传统的for循环,配合内置的len函数进行迭代。

slice := []int{1, 2, 3, 4, 5}
for i := 0; i < len(slice); i++ {
    fmt.Printf("值:%d\n", slice[i])
}

在函数间传递切片

我们知道切片是3个字段构成的结构类型,所以在函数间以值的方式传递的时候,占用的内存非常小,成本很低。在传递复制切片的时候,其底层数组不会被复制,也不会受影响,复制只是复制的切片本身,不涉及底层数组。

func main() {
    slice := []int{1, 2, 3, 4, 5}
    fmt.Printf("%p\n", &slice)
    modify(slice)
    fmt.Println(slice)
}

func modify(slice []int) {
    fmt.Printf("%p\n", &slice)
    slice[1] = 10
}

打印输出结果如下:

0xc420082060
0xc420082080
[1 10 3 4 5]

通过上面的结果可以看出,这两个切片的地址并不一样,所以可以确认切片在函数间传递是复制的。而我们修改一个索引的值后,发现原切片的值也被修改了,说明它们共用一个底层数组。

在函数间传递切片非常高效,而且不需要传递指针和处理复杂的语法,只需要复制切片,然后根据自己的业务修改,最后传递回一个新的切片副本即可,这也是为什么函数间传递参数,使用切片,而不是数组的原因。

猜你喜欢

转载自www.cnblogs.com/liujunhang/p/12534598.html