Go语言中有三种数据结构可以让用户管理集合数据,数组、切片和映射.这三种数据结构是语言核心的一部分,在标准库中广泛使用
1.数组
数组是基于切片和映射的基础数据结构,理解了数组的工作,可以理解切片和映射提供的优雅和巨大的功能
数组是一个长度固定的数据类型,可以存储一段具有相同类型的元素的连续块,数组存储的类型可以是内置类型,如整型或者字符串,或者是某种结构类型
数组结构如下,有着下标和对应数据类型的元素
数组是一个非常有用的数据结构,可以占用的内存是连续分配的,内存连续,所以CPU缓存更加方便
而且利用索引,也可以很快的找到任意的数据
声明和初始化
声明数组的时候需要指定存储的数据类型,同时声明存储的元素数量
声明后,存储的数据类型和长度就不能变了,如果需要存储更多的,需要重新创建一个,然后复制过去
声明的格式如下
在Go中声明变量的时候,总会使用对应的类型的零值对变量进行初始化
数组内每个元素都初始化为对应类型的零值
对应声明数组的元素的方式如下
array := [5] int {10,20,1,3,4,5}
如果使用…替代数组的长度,Go语言会根据初始化的时候数组长度来确定次数组的长度
array := […] int {10,20,1,5,3}
对于数组的使用
我们可以访问数组中任意位置的元素,只需要使用[]运算符
arrays := [5]int{1, 2, 3, 4}
fmt.Println(arrays)
array2 := arrays[2]
fmt.Println(array2)
数组的值在操作完成后,就会显示出
我们还可以声明一个所有元素都是指针的数组,使用*运算符就可以访问元素指针所指向值
常见的使用如下
arrayss := [5]*int {0: new(int),1: new(int)}
fmt.Println(arrayss)
Go语言中,数组是一个值,意味着数组可以用在赋值操作中,变量名代表整个数组,同样类型的数组可以赋值给另外一个数组,代码清单如下
数组的赋值如下
声明包含5个元素的指向的整数的数组
用整形指针初始化索引为0和1的数组元素
array := [5]*int {0:new(int),1:new (int)}
var array1 [5]* int
array1 = array2
复制之后,两个数组的值完全一样
对于数组之间的复制,必须要要求,数组的长度和数组的类型是相同,只有相同的,才能彼此复制
比如,不同长度,不同类型的数组就不可以
对于指针,则是同样如此,只不过复制的是指针而不是值本身
复制之后,两个数组指向同一组字符串
多维数组
数组可以创建多维数组,声明如下
func main() {
//声明多维数组
var array [4][2]int
//使用数组字面量来初始化一个二维整型数组
array := [4][2]int {{10,11},{20,21}}
//声明并初始化外层数组中索引为1和3的元素
array := [4][2]int {1:{1,1},3:{3,3}}
fmt.Println(array)
}
对于访问,则是如下
array[1][1] = 10
array[2][1] = 20
//声明多维数组
var array [4][2]int
//使用数组字面量来初始化一个二维整型数组
array = [4][2]int{{10, 11}, {20, 21}}
//声明并初始化外层数组中索引为1和3的元素
array = [4][2]int {1:{1,1},3:{3,3}}
fmt.Println(array)
var array1 [4][2]int
array1 = array
array[1][1] = 10
array[2][1] = 20
fmt.Println(array)
fmt.Println(array1)
[[0 0] [1 1] [0 0] [3 3]]
[[0 0] [1 10] [0 20] [3 3]] [[0 0] [1 1] [0 0] [3 3]] |
综上所述,可以看出我们在复制的时候,是深拷贝
甚至可以赋值单一维度
var arrays [2] int = array1[1]
在函数之间传递数组
在函数之间传递数组是一个开销很大的问题,函数之间传递变量的时候,传递的是值,也就是深拷贝了一下,如果是一个数组,就需要进行深拷贝过去
所以如果声明传入是一个100万个int类型的数组,在64位架构上,需要800万字节,传递过程中,也需要占用如此大的内存
每次函数被调用的时候,都需要在栈上分配8MB的内存,之后,整个数组的值就被复制到刚刚分配的内存
所以内存的占用和消耗太大了
我们不如直接使用指针来在函数之间传递大数组
代码基本如下
var array2 [1e6]int
foo(&array2)
}
func foo(array *[1e6] int){
fmt.Println(array)
}
这样函数接收一个100万整型值的数组的指针,利用了内存,不过由于传递的是指针,所以改变指针指向的值,会改变共享的内存
2.切片
切片是一种数据结构,便于使用和管理数据集合,围绕动态数组的概念构造的
按需自动增加和缩小内部的长度
切片的动态增长是可以通过内置的函数append来实现的,这个函数可以快速的增长切片,或者缩小切片,切片的底层内存是在连续块中分配的,所以切片还能获得索引
切片的内部实现,对底层的数组进行了抽象,提供了相关的操作方法,内部有三个字段的数据结构
分别是指向底层数组的指针,切片访问的元素的个数,切片允许增长到的元素个数
创建切片
常见的创建切片的方式是利用内置的make函数,使用make的时候,传入一个参数,指定切片的长度
slice := make([] string,5)
如果只指定长度,那么切片和容量和长度相等
也可以分别指定长度和容量
sliece := make([] int ,3,5)
创建的切片和底层数组的长度是指定的容量
如果基于切片去创建新的切片,新的切片会和原有的切片共享底层数组
所以是类似指针的操作?
slice := make([]int, 3, 3)
var slice2 []int
slice2 = slice
slice[1] = 2
slice[2] = 2
fmt.Println(slice)
fmt.Println(slice2)
[0 2 2]
[0 2 2]
还有就是容量不能小于长度,不然编译时候会报错
slice := make([]int, 5, 3)
var slice2 []int
另外的创建切片的方法是利用切片的字面量
slice := [] string {“Red”,”Blue”,”Green”,”Yellow”,”Pink”}
对于声明一个值为nil的切片,只需要在声明的时候不做任何的初始化
就会创建一个nil切片
var slice [] int
nil切片是很常见的创建方法,可以用于在出现异常的时候,返回这个切片
声明空切片和nil切片类似
slice := make([] int ,0)
slice := [] int{}
nil切片和空切片基本操作是一致的
如何使用切片
对切片中某个索引指向的元素复制和对数组中某个索引指向的元素赋值方法一致
slice := [] int {10,20,24,5,4,12}
改变索引为1的元素的值
slice[1] = 25
切片可以进行切分
slice := [] int {10,20,30,30,40}
newSlice := slice[1:3]
这样,新旧两个切片共享这一个底层数组
第一个切片能够看到全部的问个元素,后面新的数组就看不到
新的切片无法访问到指向的底层数组的第一个元素
新的切片只能访问拿到长度内的元素,试图访问超过其长度的元素会导致语言运行异常
新切片的长度和容量分别是
如果新的切片的容量为k的切片 slice[i:j]
长度 j-i
容量 k-i
就好比,newSlice := sliect[1:3]
原本的slice的容量是5
那么newSlice的
长度就是 3-1 = 2
容量就是 5-1 = 4
对于切片增长,因为切片引入了动态数组的概念,所以可以再调用append的来动态的对数组进行扩容
使用append,需要一个切片和追加的值
append调用的时候,会返回一个包含了修改结果的新切片,
slice = append(slice,10)
如果是具有共享数组的操作,则可能操作是会合并的
比如我们有一个新的切片newSlice,这时候我们去给新切片添加一个数字,由于新切片和之前的切片共享一个数组,所以可能合并导致旧的切片中的值也被改动了
上面就是给新的切片添加了一个数字,结果导致共享的数组中的老切片的值也被改动了
如果切片的底层数组没有合适的容量,那么共享数组会直接创建一个新的数组,然后复制到新的数组中
对于append后创建的新数组,go对于新数组的容量也是通过一定的设定的,当切片的容量小于1000个的时候,会成倍的增加容量,如果元素的个数超过1000个的时候,增加因子会设置为1.25
在创建切片的时候,我们还可以设置切片数组的最大容量
比如我们创建一个切片,切片的容量和长度都是5
source := [] string {“Apple”,”Orange”,”Plum”,”Banana”,”Grape”}
然后我们创建一个子切片
source := source[2,3,4]
那么就指定了容量到了Banana
我们计算一下长度,就是
j-i 3-2 = 1
k-i 4-2 = 2
如果试图设置的容量比可用的容量还要大的话,那么会得到一个语言运行时错误
然后,一旦我们设置了最高容量,再之后使用append的时候,如果没有可用容量,会分配一个新的底层数组,避免一致公用同一数组的问题
比如我们,设置了如下的子切片
source := [] string {“apple”,”orange”,”plum”,”grape”}
slice := source[2:3:3]
slice = append(slice,”cherry”)
我们在调用append之后,就会创建一个新的底层数组,这个数组包含了两个元素,并将复制了plum,然后又追加了新的cherry,返回了这样的一个新切片
然后是迭代数组的操作
切片是一个集合,必然可以迭代其中的元素,Go语言可以利用range,来配合迭代其中的元素
slice := [] int {10,20,12,13}
for index,value := range slice {
fmt.Printf(“Index : %d Value: %d\n”,index,value)
}
range会返回两个值,一个是索引位置,另一个是元素值的一个副本
注意,这是副本,而不是对源元素进行的操作,如果直接使用这个值进行操作,就会造成错误
我们看一个测试
value的地址总是相同的,所以没法这样直接操作
如果不需要索引值,可以使用占位符来忽略这个值
for _,value := range slice{
fmt.Printf(“Value: %d”\n”, value)
}
关键字range总是从切片头部开始迭代,如果想要进行更多的控制,可以使用传统的for循环
for index:=0; index<len(slice) ; index++ {
fmt.Printf(“value: %d, index: %X \n”,slice[index],index)
}
利用了内置函数len,来处理数组切片 通道,内置函数有len 和 cap,cap返回切片的容量
接下来是多维切片
组合多个切片形成多维切片
创建一个整形切片的多维版本
slice := [][] int{{10},{10,200}}
我们有了一个包含两个元素的外层切片
内部是独立的子切片,整体的结构如下
这种组合可以让用户创建复杂且强大的结构
而且可以直接append
slice[0] = append(slice[0],20)
这样我们使用append函数追加的时候,先增长切片,然后将新的整型切片复制给外层切片中的第一个元素,当操作完成后,会成为新的整形切片分配新的底层数组,将切片复制到外层切片的索引为0的元素
在函数之间传递切片
函数之间传递切片是以值的方式传递,但是传递的成本很低,我们可以创建一个大的切片然后以值的方式传递给函数foo
在64位架构的机器上,一个切片需要24个字节的内存,指针需要8个字节,长度和容量需要8字节,不过传递的时候,传递的是外表,实际底层数组是共享的
复制的时候只会复制切片本身,不会涉及底层数组
再函数之间传递24字节的数据和快速简单,不需要传递指针和处理复杂的语法,只需呀复制切片,然后共享内部的底层数组
3.映射
一种基础的数据结构,存储一系列的无序键值对
基于键来存储值,来方便我们基于键来快速的检索数据
键就好比索引,指向和这个键相关的值
内部是一个集合,可以使用类似处理数组或者切片的方式进行迭代,但本身无须,所以无法预测顺序
这是因为内部采用了散列表
映射的散列表就是一组桶,在存储和删除或者查找键值对的时候,需要先选择一个桶,这个选择的方式是利用了散列函数,来获取桶的位置,我们可以生成一个索引
索引最终将键值对分布到所有可用的桶中
随着映射存储的增加,索引会愈发的分布均匀,访问的速度越来越快,
字符串转换为一个散列值,这个数值会落在映射到已经有的桶的序号范围内可以用于存储的桶的序号,之后,就被用于选择桶,存储或者查找指定的键值对,对于Go的映射来说,生成的散列数的一部分来选择桶,应该是散列数的低位用来选择桶
上面,使用了两个数据结构来存储数据,一个数据结构是一个数组,用来选择桶的散列键的高八位值,第二个数据结构是一个字节数组,先是存储了这个桶里所有的键,然后是桶里所有的值,实现这种键值对的存储方式用于减少每个桶所需的内存
创建和初始化映射
对于创建并初始化映射,可以考虑使用内置的make函数
dict := make(map[string]int)
或者直接使用键值对进行初始化映射
dict := map[string]string {“Red”: “#da1337”, “Orange”: “#e95a22”}
这种方式创建的映射的初始按长度,会根据你初始化的时候指定的键值对的数量来指定
映射的键可以是任何的值,可以是内置的类型或者是结构的类型,只要可以使用==运算符来比较
不能是切片或者函数这种具有引用含义的,不能作为映射的键,使用会直接造成编译错误
不过可以作为键来使用
dict := map[int] []string{}
如何使用映射
首先是增加映射的键值对
colors := map[string]stirng{}
colors[“Red”] = “#da1337”
上面我们用于赋值的都是空映射,是可以正常使用的
除此外,我们还可以声明nil映射
nil映射是没法使用的
var colors = map[string]string{}
这样使用会发出运行时异常
对于从映射中取出value,有两个选项
一种是只是获取值,并且判断这个值是不是存在
或者使用获取值,并且在获取的同时,获取一个键是否存在的标志
对于映射,同样可以使用关键字range来进行迭代,range返回的则是一个个键值对
对于映射的删除
可以使用内置的delete函数
delete(colors,”Coral”)
对于在函数之间传递映射
对于映射的传递,并不是传递的副本,本质上,仍然是传递的散列桶结合,当有映射传递的时候,有一个对映射进行了引用修改,其他的引用都会察觉的到
关于这一点,和切片具有相同的特性
数据是构造切片和映射的基石
Go语言中的切片可以经常用来处理数据的集合
映射可以处理具有键值对结构的数据
内置函数make可以创建切片和映射,指定原始的长度和容量,可以直接使用切片和映射字面量
切片有容量限制,可以使用内置的append函数扩展容量
映射的增长无须担心容量
内置函数len可以用于获取切片或者映射的长度
cap用于切片,获取容量
通过组合,可以创建多维数组或者多维切片,可以使用切片或者其他映射来作为映射的值
切片或者映射在函数之间传递的成本小,共享底层的数据结构