go语言容器之数组与切片

870 阅读5分钟

程序中操作大量同类型变量时,为了方便数据的存储与操作,常常需要借助容器的力量。GO语言标准库提供了常用的容器实现。如固定大小的数组,可以动态扩容的切片,双向列表以及key-value形式存储的字典等。下面介绍数组与切片的基本使用。

数组

数组是一段存储固定长度的连续内存空间,它的大小在声明时就已经固定。虽然大小不能变化,但数组的成员却能修改。数组的声明样式如下:

var name [size]Type

数组大小必须指定,可以是常量或者表达式,必须在静态编译时确定其大小,不能动态指定,Type表示数组成员类型,可为任意类型。

数组的初始化可以在声明时使用初始化列表对数组进行初始化,也可以通过下标对数据成员进行访问和赋值,如下所示。

func main()  {

	var animals1 [3]string
	animals1[0] = "cat"
	animals1[1] = "dog"
	animals1[2] = "rabbit"
	fmt.Println(animals1) //[cat dog rabbit]

	animals2  := [...]string{"duck", "chicken", "cat"}
	fmt.Println(animals2) //[duck chicken cat]
}

使用初始化列表初始化数组时,需要注意[]内大小需要和{}内的数组成员的数量一致,上面的例子使用了... 让编译器根据{}的成员的数量确定数组的大小。

	animals3 := new([3]string)
	animals3[0] = "dog"
	animals3[1] = "cat"
	animals3[2] = "duck"
	fmt.Println(*animals3) // [dog cat duck]

切片

切片是对数组一个连续片段的引用,它是一个容量可变的序列。它的内部结构包括底层数组指针、大小和容量。它通过指针引用底层数组,把对数据的读写限定在指定区域内。

切片的生成方式

1.从原生数组中生成切片并修改切片成员

从原有数组中生成一个切片,那么生成的切片指针即指向原始数组,格式如下:

slice := source[begin:end]

source表示生成切片的原有数组,begin表示切片的开始位置,end表示切片的结束位置(不包含结束位),这与python中语法是一致的。示例如下:

a := [...] int {0,1,2,3,4,5,6}
aSilce := a[0:2]
fmt.Printf("aSilce value is %v\naSilce len is %v\naSilce cap is %v\n", aSilce, len(aSilce), cap(aSilce))
// 输出如下:
aSilce value is [0 1]
aSilce len is 2
aSilce cap is 7

在这个切片中,我们仅能访问长度内的值,如果访问下标超过了切片的长度,编译器会抛出下标越界的异常。我们如果对切片内的成员进行修改,因为切片作用于原数组的引用,因此原数组值会进行改变。

aSilce[0] = 4
fmt.Printf("aSlice value is %v\nSource array value is %v", aSilce,a)

// aSlice value is [4 1]
// Source array value is [4 1 2 3 4 5 6]

上面的说法其实是有一定的问题的,如果当前切片容量可以容纳更多元素,即size小于cap,添加操作指向原有数组进行修改,当切片容量不足容纳更多元素,会申请新的地址空间,后面进行演示。

2.动态创建切片

可以通过make函数动态创建切片,在创建过程中指定切片的长度和容量,示例如下:

make([]T, size, cap)

T为切片的成员类型,size为当前切片具有的长度,cap为当前切片分配的容量。示例如下:

  sli = make([]int, 2, 4)
  fmt.Printf("sli value is %v\n", sli)
  fmt.Printf("sli len is %v\n", len(sli))
  fmt.Printf("sli cap is %v\n", cap(sli))
	
 // 输出如下:
  sli value is [0 0]
  sli len is 2
  sli cap is 4

可以看出make函数创建的新切片中的成员都被初始化为类型的初始值。

3.声明新的切片

直接声明切片类似与数组的初始化,但是不需要指定其大小,否则就变成了数组,样式如下:

var name []T

此时声明的切片没有分配内存,我们可以在声明切片的同时对其进行初始化,示例如下:

sli := [] int{1, 2, 3}
fmt.Printf("sli value is %v,len is %v,cap is %v",sli, len(sli),cap(sli))
//输出如下:
sli value is [1 2 3],len is 3,cap is 3
4.向切片中添加元素

GO语言提供了append内置函数用于动态向切片中添加元素,它将返回新的切片。

package main

import "fmt"

func main()  {


	arr1 := [...]int{1,2,3,4}
	arr2 := [...]int{1,2,3,4}

	sli1 := arr1[0:2] // 长度为2,容量为4
	sli2 := arr2[2:4] // 长度为2,容量为2

	fmt.Printf("sli1 pointer is %p, len is %v, cap is %v, value is %v\n", &sli1, len(sli1), cap(sli1), sli1)
	fmt.Printf("sli2 pointer is %p, len is %v, cap is %v, value is %v\n", &sli2, len(sli2), cap(sli2), sli2)

	newSli1 := append(sli1, 5)
	fmt.Printf("newSli1 pointer is %p, len is %v, cap is %v, value is %v\n", &newSli1, len(newSli1), cap(newSli1), newSli1)
	fmt.Printf("source arr1 become %v\n", arr1)

	newSli2 := append(sli2, 5)
	fmt.Printf("newSli2 pointer is %p, len is %v, cap is %v, value is %v\n", &newSli2, len(newSli2), cap(newSli2), newSli2)
	fmt.Printf("source arr2 become %v\n", arr2)

	arr3 := [...]int{1,2,3,4}
	sli3 := arr3[0:2:2] // 长度为2,容量为2

	fmt.Printf("sli3 pointer is %p, len is %v, cap is %v, value is %v\n", &sli3, len(sli3), cap(sli3), sli3)

	newSli3 := append(sli3,5)
	fmt.Printf("newSli3 pointer is %p, len is %v, cap is %v, value is %v\n", &newSli3, len(newSli3), cap(newSli3), newSli3)
	fmt.Printf("source arr3 become %v\n", arr3)

}

输出如下:

sli1 pointer is 0xc00000c060, len is 2, cap is 4, value is [1 2]
sli2 pointer is 0xc00000c080, len is 2, cap is 2, value is [3 4]
newSli1 pointer is 0xc00000c0e0, len is 3, cap is 4, value is [1 2 5]
source arr1 become [1 2 5 4]
newSli2 pointer is 0xc00000c120, len is 3, cap is 4, value is [3 4 5]
source arr2 become [1 2 3 4]
sli3 pointer is 0xc00000c160, len is 2, cap is 2, value is [1 2]
newSli3 pointer is 0xc00000c1a0, len is 3, cap is 4, value is [1 2 5]
source arr3 become [1 2 3 4]

我们发现容量足够的sli直接将append添加的新元素覆盖到原有的数组arr1中,而容量不足的sli2进行了扩容操作,申请了新的底层数组,不在原有的数组上进行操作了,实际使用中要特别留意。