初识go语言之 数组与切片(创建,遍历,删除,插入,复制)

  • Post author:
  • Post category:其他



1、数组

go语言的数组与其他语言的数据定义基本类似:大小确定,内存连续,可以随机访问。数组的元素使用操作符[]来索引。首元素是array[0],最后一个元素是array[len(array)-1]。



1.1数组的创建

数组的创建有下面三种方式

[length]Type
[N]Type{value1, value2, ..., valueN}
[...]Type{value1, value2, ..., valueN}

对于数组而言,它的长度和容量的大小是相等的,也就是len(array)和cap(array)的返回值相同,对于最后一种不指定大小的情况下,程序会自动计算数组的大小。所有情况下,数组在创建后大小不能被改变

var array1 [5]int
var array2 [2][2]int
array1[0], array1[1], array1[2], array1[3] = 4, 2, 1, 6
array2[0][0], array2[0][1] = 2, 1

array3 := [3]int{1, 3, 5}
array4 := [...]int{4, 1, 5, 6, 7}
fmt.Printf("array1 size[%d] cap[%d]\n", len(array1), cap(array1))
fmt.Printf("array2 size[%d] cap[%d]\n", len(array2), cap(array2))
fmt.Printf("array3 size[%d] cap[%d]\n", len(array3), cap(array3))
fmt.Printf("array4 size[%d] cap[%d]\n", len(array4), cap(array4))

//output
array1 size[5] cap[5]
array2 size[2] cap[2]
array3 size[3] cap[3]
array4 size[5] cap[5]



1.2 数组的遍历

数组的遍历有两种方式:

array := [5]int{1, 3, 5}
//方式一、
for i := 0; i < len(array); i++{
    fmt.Printf("index array[%d] = %d\n", i, array[i])
}

//方式二、
for index, val := range array {
    fmt.Printf("index array[%d] = %d\n", index, val)
}

//output
index array[0] = 1
index array[1] = 3
index array[2] = 5
index array[3] = 0
index array[4] = 0



1.3数组使用注意点

与C/C++不同,go语言数组的工作方式有个重要的差异



1)数组是值,将一个数组赋值给另一个数组,会拷贝所有的元素


2)数组作为函数参数,收到的是数组的一个拷贝,而不是它的指针


3)数组的大小是类型的一部分,[10]int和[20]int是不一样的

针对第二种,这里特别说一下,你可以用类似于C的方式,将数组的指针作为函数参数传入来防止拷贝,但这不符合go语言的书写习惯,很不go。在go语言中我们使用切片



2、切片(slice)

一个slice是一个数组的部分引用,其底层还是一个数组。在内存中它是一个包含三个域的结构体:指向数组的指针,切片的长度,切片的容量。如下图所示:

引入切片主要是为了消除数组的固定长度的限制,使用者可以不需要手动指定大小,切片会自动调整自己的大小。将切片和数组放在一起讲主要是因为他们的很多共性是一致的,但又存在各自的特性。下面我们来细说切片



2.1 切片的创建

切片有下面四种创建方式

make([]type, length, capacity)
make([]type, length)
[]type{}
[]type{value1, value2,..., valueN}

通过第3、4两种创建方式我们可以知道,数组和切片的区别就是是否指定了大小,

slice1 := make([]int, 4, 6)
fmt.Printf("slice1 addr %p size[%d] cap[%d]\n", slice1, len(slice1), cap(slice1))

slice2 := make([]int, 4)
fmt.Printf("slice2 addr %p size[%d] cap[%d]\n", slice2, len(slice2), cap(slice2))

slice3 := []int{}
fmt.Printf("slice3 addr %p size[%d] cap[%d]\n", slice3, len(slice3), cap(slice3))

slice4 := []int{1, 2}
fmt.Printf("slice4 addr %p size[%d] cap[%d]\n", slice4, len(slice4), cap(slice4))

//output
slice1 addr 0xc04200c600 size[4] cap[6]
slice2 addr 0xc042008560 size[4] cap[4]
slice3 addr 0x55c988 size[0] cap[0]
slice4 addr 0xc04200a240 size[2] cap[2]

slice3是一个空切片。第二种方式没有指定capacity的值,其默认值就是length的值



2.2 切片的操作

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处获得的切片

不管是哪一种操作,新获得的切片跟源切片底层对应的数组都是同一个,所以对其中一个切片元素的修改也会影响到另外一个

slice := []int{10, 20, 30, 40, 50}
fmt.Printf("slice addr:%p len:%d cap:%d slice:%v\n", slice, len(slice), cap(slice), slice)

newSlince := slice[1:3]
newSlice[0] = 999
fmt.Printf("slice addr:%p len:%d cap:%d slice:%v\n", slice, len(slice), cap(slice), slice)


slice addr:0xc042072090 len:5 cap:5 slice:[10 20 30 40 50]
slice addr:0xc042072090 len:5 cap:5 slice:[10 999 30 40 50]

如下图所示:



2.3 切片的遍历

切片的遍历跟数组类似,两种遍历方式

//方式一、
for i := 0; i < len(slice); i++{
    fmt.Printf("index slice[%d] = %d\n", i, slice[i])
}

//方式二、
for index, val := range slice {
    fmt.Printf("index slice[%d] = %d\n", slice, val)
}



2.4 修改切片元素值

切片底层还是一个数组,所以修改切片的元素可以直接通过下标索引的方式来操作。

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[1] = 101
fmt.Printf("slice addr:%p len:%d cap:%d slice:%v\n", slice, len(slice), cap(slice), slice)

//output
slice addr:0xc042008540 len:4 cap:4 slice:[1 2 3 100]
slice addr:0xc042008540 len:4 cap:4 slice:[1 101 3 100]



2.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



2.6切片的插入和删除

切片的增加主要还是通过append函数来完成的,总体上就是将切片拆分,拼接再组合的过程。下面分别举例说明



2.6.1 删除

删除下标为index的元素

slice = append(slice[:index], slice[index+1:]...)

实例如下:

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)

//output
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:]...)



2.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)

//output
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)))

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)

//output
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时,复制操作不会进行任何操作

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)

//output
copy_slice addr:0xc0420720c0 len:0 cap:6 slice:[]



版权声明:本文为fm_VAE原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。