首发于 Go菜鸡

数组、切片、映射的内部实现和基础功能

数组、切片和映射是Go语言用来管理集合数据的三种数据结构,在标准库中广泛使用,掌握如何使用这些数据结构,对于快速、正确地编写Go代码十分重要。

数组

数组是一种长度固定的数据类型,用于存储一段具有相同数据类型的元素的连续块。储存的数据类型可以是内置类型或某种结构类型。数组占用的内存是连续分配,而且每个元素的类型相同,访问元素时在内存中可以以固定的地址索引,速度非常块。

数组一旦声明,数据类型和长度都不能改变。如需更长的数组,需要创建新的数据再把旧的数据赋值。只有数组的长度和数据类型都相同的变量,才能互相赋值。

var array[5]int			//声明数组,并设置为零值
array := [5]int{10,20,30,40,50}	//使用数据字面量声明数组
array := [...]int{10,20,30} 	//自动计算数组长度
v := array[2] //从指定的数组元素取值
array[2] = 25 //对指定的数组元素赋值

数组是切片和映射的基础数据结构。

切片

切片是由动态数组的概念组成的,可以按需增加或缩小。之所以称为切片,是因为创建一个新的切片就是把底层数组切出一部分。

切片是有三个字段的数据结构:指向底层数组的指针、切片的元素的个数(长度)、切片的元素允许增长到的大小(容量)。

slice := make([]string, 5)  //长度和容量都是5元素的字符串切片
slice := make([]int, 35)  //长度为3个元素,容量为5个元素的整型切片
slice := []int{}
slice := []string{"red", "blue", "green"} //通过切片字面量来声明切片,其长度和容量都是3个元素
  • 对切片里某个索引指向的元素赋值,跟数组一样,都是通过[]操作符。
  • 切片只能访问到其长度内的元素,访问超出其长度的元素将会导致异常。
slice := []int{10,20,30,40,50} //创建一个整型切片,其长度和容量都是5个元素
newSlice := slice[1:3] //新的切片,长度为2,容量为4,元素值:20,30
如何计算新切片newSlice的长度和容量
长度:j-i
容量:k-i
k是slice底层数组的容量,执行newSilce := slice[i:j],对于newSilce来说:
长度: 3-1=2
容量: 5-1=4

数组声明后无法改变其长度,但切片可以通过append按需增加容量。append会处理底层数组的 容量增长 。当切片没有可用容量且容量少于1000个元素时,append会成倍地增加底层数组的容量,超出1000则增加25%的容量。

当切片底层数组还有可用容量时,使用append操作会将新元素合并到当前切片。

slice := []int{10,20,30,40,50}
newSlice := slice[1,3] //新的切片,长度为2,容量为4,元素值:20,30
newSilice := append(newSlice, 60) //20,30,60
//注意:两个切片共享同一段数组,修改其中一个切片,对另一个切片也能感知。

当切片底层数组没有可用容量时,使用append操作会创建一个新的底层数组,将现有的值赋值到新数组,再追加新的值。

slice := []int{10,20,30} //容量为3
newSilice := append(slice, 60) //拥有一个全新的底层数组,容量为6。 10,20,30,60

创建切片时设置 容量限制 ,可以强制让append操作后的新切片创建新的底层数组,就不用担心对新切片的操作会影响其他切片的值。

source := []int{10,20,30,40}
slice := source[2:3:3] //slice的长度和容量都是1个元素
slice := append(slice, 80) //创建一个新的底层数组,包含两个元素:30,80
对于slice[2,3,3]或slice[i:j:k]
长度:3-2=1 即 j-i
容量:3-2=1 即 k-i

迭代切片

使用for range从切片的头部开始迭代元素,range是创建了每个元素的副本,而不是返回该元素的引用。

slice := []int{10,20,30}
for index,value range slice{
    fmt.Printf(index,value)

使用for循环可以从任意索引开始迭代切片,len返回切片的长度,cap返回切片的容量。len,cap可以用于处理数组、切片、通道。

slice := []int{10,20,30}
for index := 1; index < len(slice);index++{
    fmt.Printf(index,slice[index])

映射

映射是用于储存无序键值对的数据结构, 能够通过索引键快速检索数据。映射是无序的集合,虽然可以使用处理数组和切片的方式迭代映射,但对同一个映射的每次迭代返回的键值的顺序可能不一样。

dict := make(map[string]int) //键的类型是string,值的类型是int
colors := map[string]string{"red":"#dd2255", "order":"#e95a22"}  //键和值的类型都是string,并初始化
dict := map[int][]string{} //映射的键类型为int,值的类型为字符串切片
//从映射里获取值并判断是否存在
value,exists := colors["blue"]
if exists{
    fmt.println(value)
//使用for range迭代映射
for key,value := range colors{
    fmt.Printf(key,value)

注意:切片、函数、包含切片的结构类型具有引用语义,不能作为映射的键。


在函数间传递数组、切片、映射

在函数中传递变量时,总是以值的方式传递。

如果传递的变量是数组,那么不管数组有多长都会完整复制,再传给函数,这样的内存开销非常大。虽然Go会处理赋值操作,但 传入指向数组的指针 ,会大大减少内存的分配,比如在64位机器上,100万个整型值的数组需要分配8M内存,但是指向这100万个数组值的指针只需8字节。

var arr [1e6]int
f1(arr) //传递指向数组的值
func f1(arr [1e6]int){