GO语言之数组切片列表映射
本章将以实用为目的,详细介绍数组、切片、映射,以及列表的增加、删除、修改和遍历的使用方法。
数组:
数组是一段固定长度的连续内存区域,在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化,除非明确数组的长度,否则Go语言中很少使用数组
一维数组
1、声明数组:
数组的写法如下:
var 数组变量名[元素数量] T
- 数组变量名: 数组声明及使用时的变量名。
- 元素数量: 数组的元素数量。可以是一个表达式,但最终通过编译期计算的结果必须是整型数值。也就是说,元素数量不能含有到运行时才能确认大小的数值。
- T 可以是任意基本类型,包括T 为数组本身。但类型为数组本身时,可以实现多维数组。
下面的例子演示了数组的使用,如图:

打印结果如下:

说明:将team 声明为包含3 个元素的字符串数组,第2 ~4 行,为team 的元素赋值。数组一旦声明了执行长度,就不能添加超过长度的元素,否则将报错
2、初始化数组:
(1)、数组可以在声明时使用初始化列表进行元素设置, 参考下面的代码:
var team= [3]string{ " hammer ","soldier ","mum " }
这种方式编写时, 需要保证大括号后面的元素数量与数组的大小一致,如果不一致那么未初始化的元素的值降为默认值


(2)、初始化也可以交给编译器,让编译器在编译时,根据元素个数确定数组大小。
var team= [...]string{ " hammer ","soldier ","mum " }
说明:”…” 表示让编译器确定数组大小。上面例子中, 编译器会自动为这个数组设置元素个数为3
例如:下面的例子在初始化数组的时候设置了元素的值,但并未指定元素个数,现在遍历元素个数,如图:

运行后的结果如下:

从上图结果可以看出,编译器自动为数组设置元素个数为4个,也可以通过fmt.Println(len(team))打开数组长度,结果为4
(3)、初始化数组也可以在函数内部通过短变量的形式:如下:
var := [5]string{"hello","world","abcd"}
如果声明数组的时候,采用多行的方式,那么每一行最后都要以逗号结尾,最后一行也是,否则报错,如图:

(4)、初始化数组的时候,根据索引指定对应的值,未指定的为零值
下图中声明的数组索引2对应的值为8,索引3对应值为7,索引5对应值为9,其余为零值,如图:


也可以部分使用索引初始化,部分指定实际呃值,如下:
array := [...]int{1,2,3:7, 5:9} //结果:[1,2,0,7,0,9]
3、遍历数组:
遍历数组可通过for循环进行遍历,如图:

遍历后的结果如下:

上述例子中使用了range函数,当使用range来遍历数组的时候,range函数返回索引和元素,遍历字典的时候,返回字典的键和值
数组索引从0开始,因此访问数组第一个元素需要使用索引0,访问数组第二个元素使用索引1
二、二维数组
如果一个一维数组中的所有元素都是一维数组,那么这个一维数组就是一个二维数组,二维数组由行和列组成的二维表结构,行表示这个二维数组中有多少个元素,列表示每个元素中的列
1、声明二维数组,语法如下:
var array [4][3]int #此二维数组包含4个一维数组,每个一维数组中三个元素
2、初始化二维数组


注意:上述arrays的最后一个大括号如果没有在{10,11,12}的后面,那么在{10,11,12}后面就要添加一个逗号,否则编译报错,如果最后一个大括号直接写在{10,11,12}后,就不用加逗号,如图:

3、短变量声明二维数组,如图:

注:第14行的大括号是在单独一行,因此第13行后需要使用逗号
4、访问二维数组,通过for循环,如图:


切片:
在Go语言中,使用数组存在一定的局限性,数组声明长度后就无法继续添加元素,然而切片比数组更加灵活,可以在切片中添加和删除元素,还可以复制切片中的元素,可以将切片视为一个轻量的数组包装器,它既保留了数组的完整性,又比数组使用起来更加灵活
Go 语言切片的内部结构包含地址、大小和容量。切片一般用于快速地操作一块数据集合,切片是动态结构,只能与nil判定相等, 不能互相判相等,如果要判断两个切片是否相等,可以使用reflect.DeepEqual()函数
在go语言中,切片是引用类型,如果将一个切片赋值给另一个切片,它们共用一个底层数组,因此修改一个另一个也会变化,如图:


如果想避免这种情况,可以使用copy来创建一个新的底层数组的切片,如图:

从结果可以看出,修改后b的值变了,a的值没有变,如图:

1、从数组或切片生成新的切片:
切片默认指向一段连续内存区域,可以是数组,也可以是切片本身
从连续内存区域生成切片是常见的操作,格式如下:
slice [开始位置: 结束位置]
- slice 表示目标切片对象
- 开始位置对应目标切片对象的索引
- 结束位置对应目标切片的结束索引
例如:从数组生成切片,代码如下:

通过切片可以获取到指定元素,执行后的结果如下:

从数组或切片生成新的切片拥有如下特性:
- 取出的元素数量为:结束位置-开始位置
- 当缺省开始位置时,表示从连续区域开头到结束位置
- 取出元素不包含结束位置对应的索引,切片最后一个元素使用slice [len(slice)-1]获取
- 当缺省结束位置时,表示从开始位置到整个连续区域末尾
- 两者同时缺省时,与切片本身等效
- 两者同时为0 时,等效于空切片,一般用于切片复位
- 根据索引位置取切片slice 元素值时,取值范围是( 0~ len(slice)-1 ),超界会报运行时错误。生成切片时,结束位置可以填写len(slice)但不会报错
例如:下面的例子表示获取切片操作,如图:

注:go中切片最后一个元素不能用-1来获取,需要用len(team)-1 来获取
2、声明切片:
每一种类型都可以拥有其切片类型,表示多个类型元素的连续集合。因此切片类型也可以被声明。切片类型声明格式如下:
var name [] T
- name 表示切片类型的变量名
- T 表示切片类型对应的元素类型
可以理解为[]中有数字为数组,无数字为切片
例如:下面代码展示了切片声明过程,如图:

说明如下:
- 第一行声明一个字符串切片,切片中拥有多个字符串。
- 第二行声明一个整型切片,切片中拥有多个整型数值。
- 第三行声明为一个整型切片。本来会在”{}”中填充切片的初始化元素,这里没有填充,所以切片是空的,但是此时empty已经被分配了内存,但是没有元素
- 第四行切片均没有任何元素, 3 个切片输出元素内容均为空
- 第六行声明但未使用的切片的默认值是nil ,因此比较结果为true
- 第八行empty已经被分配到了内存,但没有元素,因此和nil 比较时是false
执行后输出结果如下:

使用var来声明切片后,在修改切片元素值的时候,如果不使用make初始化切片,否则会异常,如图:


因为切片未初始化,此时切片为nil,无法修改元素,修改下上述代码,使用make初始化切片,如图:


注意:如果使用不使用make,而是直接append来追加元素,就不会报错,因为append为追加元素,而slice[0]实际相当于修改索引为0的元素
下面例子表示通过短变量形式声明字符串类型切片,如图:

执行结果如下:

3、使用make()函数构造切片
如果需要动态地创建一个切片,可以使用make()内建函数,语法格式如下:
make ( []T , size, cap )
说明:
- T : 切片的元素类型
- size : 就是为这个类型分配多少个元素(byte),比如一个字母一个字节,一个汉字三个字节,根据需求自定义设置,不一定越大越好,大了反而浪费空间
- cap:预分配的元素数量,这个值设定后不影响size , 只是能提前分配空间,降低多次分配空间造成的性能问题。
例如:通过如下示例展示如何通过make()函数创建切片,如图:

输出结果如下:

注意:容量不会影响当前的元素个数,因此a和b的len都是2,但是如果容量为10,实际为2,那就会影响切片长度,如图:


使用make()函数生成的切片一定发生了内存分配操作。但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位直, 不会发生内存分配操作。切片不一定必须经过make()函数才能使用。生成切片、声明后使用append()函数均可以正常使用切片。
注意:如果在用bufio中的Read(b []byte)将数据读取到切片b中时,需要提前通过make初始化切片并分配空间,如果分配空间过大,后续将切片反序列化到结构体中会报错,此时可通过Read(b []byte)返回的实际写入字节数来对切片b进行裁剪来获取实际长度的切片然后进行反序列化即可
4、使用append()函数为切片添加元素
Go 语言的内建函数append()可以为切片动态添加元素。每个切片会指向一片内存空间,这片空间能容纳一定数量的元素。当空间不能容纳足够多的元素时,切片就会进行”扩容” , “扩容”操作往往发生在append()函数调用时.
切片在扩容时, 容量的扩展规律按容量的2 倍数扩充,例如l 、2 、4 、8 、16· ·····
例如:下面通过append()函数进行切片扩容,number表示原切片,后面i表示新增元素,如图:

代码输出结果如下:

从上图可以看出len()函数并不等于cap,append()函数也可以一次性添加多个元素。
下面的例子同样演示了如何给切片添加元素,如图:


从上图中可看出,声明切片的长度为2,但是必要的时候可以向切片动态的添加元素,调整切片的长度
下面例子表示为切片扩展5个元素,如图:


向切片添加多个元素: slice := append(slice,”hello”,”world”,”nihao”,”shijie”)
也可以通过append向切片的前面添加元素,其实就是把原切片打散加入到新切片中,如下:
a := []int{4,5,6}
a = append([]int{1},a...) //结果就是[1,4,5,6]
注意:向切片开头添加元素会重新分配内存,所有切片的元素要重新复制一遍,因此性能会比向切片尾部追加元素差很多,不建议使用
5、复制切片元素到另一个切片
使用Go 语言内建的copy()函数,可以迅速地将一个切片的数据复制到另外一个切片空间中, copy()函数的使用格式如下:
copy( destSlice, srcSlice []T) int
- srcSlice 为数据来源切片
- destSlice 为复制的目标。目标切片必须分配过空间且足够承载复制的元素个数,来源和目标的类型一致, copy 的返回值表示实际发生复制的元素个数。
例如:下面的代码将演示对切片的引用和复制操作后对切片元素的影响,如图:

代码输出结果如下:

向切片中指定位置插入一个元素:


为什么要扩展一个空间?如果不扩展空间那么使用copy复制切片从索引3开始的元素个数为3个,复制到目标的索引4开始的元素位置的元素只有两个位置,如果不扩展元素,那么就会丢失一个元素,实际复制的结果就是4、5,6被舍弃了,因此要扩展一个元素
向切片指定位置插入多个元素,也就是插入一个切片,如图:


6、从切片中删除指定元素
Go 语言并没有对删除切片元素提供专用的语法或者接口, 需要使用切片本身的特性来删除元素
方法一:通过截取法来删除
(1)、下列代码删除索引为2的元素,如图:

代码输出结果如下:

除非确定必须使用数组,否则请使用切片,切片能够让您轻松地添加和删除元素,还无须处理内存分配问题,上面的三个点可以理解为将字符串切片中的每个字符串提取出来作为参数传入
(2)、下面例子表示删除切片中元素world,也是根据索引,如图:


(3)、根据索引删除切片中多个元素,下面例子是删除第2和第3个元素,如图:

删除后的切片内容如下:

注意:删除切片中的多个元素的时候,for循环要从最大开始,因此上面的 i 的值初始值为1
(4)、通过copy()来删除切片中开头的元素,如图:


(5)通过append()来删除切片中指定索引的元素,如图:


注:如果要删除中间多个N个元素,可以写成append(b[:i],b[i+N]…)
(6)、如果切片中元素的是指针对象,删除元素依然被底层数组引用,没有被垃圾回收器及时回收,如图:


解决办法是可以先将要回收内存的指针设置为nil,保证垃圾收集器可以发现需要回收的对象,然后再执行删除操作,如图:


注:如果切片的生命周期很短,可以不必在意这个问题,因为切片已经被回收,元素自然就被回收
方法二:通过移位法删除(推荐,性能好)
此移位法会修改原切片,如果不想修改原切片就不能使用此方法,可以使用第一种的截取法

运行结果如下:

方法三、从一个切片中删除另一个切片中的元素
1、下面定义两个切片a和b,现在将a中和b元素相同的元素删除,如图:


7、删除切片中重复的元素
删除切片中重复的元素可借助map来实现,因为map中的key是唯一的,因此可将切片中的元素作为map的key
下面例子表示删除切片a中重复的元素hello,如图:

运行结果如下:

声明切片小例子:
var s []string //字符串切片,切片中每个元素都是字符串
var s []struct //结构体切片,切片中每个元素都是结构体
var s []*struct //结构体指针切片,切片中每个元素都是结构体指针
下面例子表示定义结构体切片,并打印,如图:


再次注意:切片后面放三个点表示将切片拆散
8、下面例子表示map类型的切片,如图:

运行结果如下:

除了上述的append方法添加外,还可以通过指定元素的方式来向切片中添加元素,如图:


注意:map必须要初始化才能使用,因此第一个第二个元素都要先初始化然后才可以添加元素
9、将两个切片合并为一个切片


10、判断切片中是否有重复元素
下面例子判断切片中是否有重复元素,并打印出来重复元素,如图:


11、比较两个切片中元素的长度,并将最长的元素长度提取到新切片中,如图:

- 将切片c的值赋值给切片a
- 11行:将切片传递给compare进行比较
- 17-23:循环比较a与b的元素长度,并将最长的添加到d中

注:切片a被重新赋值后,内存地址是不变的
12、获取一个切片的元素在另一个切片中的索引
下面例子中获取切片a中的元素在切片b中的索引,如图:


13、将二维切片中重复的元素设置为空字符串,原理就是根据map来进行判断,如图:

输出结果中的空格就是被修改为空字符串的元素,如图:

本例子中演示的是包含两个切片的二维切片,实际包含任意数量的切片都是可以实现的
14、判断两个切片是否相等,使用reflect.DeepEqual,如图:


注:切片进行比较的时候,元素内容,元素位置必须都一样,否则就不相等
15、读取json数据到字节切片中
使用make([]byte,1024)创建字节切片的时候,如果实际读取的字节数量较大,那么这个切片可能放不下会出现截断的情况,要么扩大容量,但是也可能造成浪费,此时可以通过io.ReadAll()来动态调整字节切片大小,如图:

- result.txt的内容为json数据
- by 为读取后生成的字节切片
映射(map)
map是一种无序的基于key-value的数据结构,也可以理解为字典,Go语言中的map是引用类型,必须初始化才能使用
Go 语言提供的映射关系容器为map 。map 使用散列表( hash )实现。
1、添加关联到map 并访问关联和数据:
Go 语言中map 的定义是这样的:
map[KeyType]ValueType
- KeyType 为键类型,可以为任意类型,但是必须可通过==进行比较的类型
- ValueType 是键对应的值类型
map类型的变量默认初始值为nil,需要使用make()函数来分配内存,语法为:make(map[KeyType]ValueType, [cap]) ,其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量
一个map 里,符合KeyType 和ValueType 的映射总是成对出现
注意: map中如果key重复了,后面添加的key会覆盖前面添加的key,最终只有一个key
下面例子展示了map的使用情况,如图:

代码输出结果如下:

下面例子表示初始化map时候配置了cap容量,如图:


注意:某些情况下, 需要明确知道查询中某个键是否在map 中存在,可以使用一种特殊的写法来实现,看下面的代码:

在默认获取键值的基础上,多取了一个变量ok , 可以判断键route2是否存在于map 中,代码输出结果如下:

从输出结果可以看出,除了输出默认值0外,还输出了false,表示map中不存在此元素,在实际书写代码时候可以直接用if scene[“route2”] {} 来写,表示如果存在,那么执行{}中程序
map 还有一种在声明时填充内容的方式(声明式填充不用make),代码如下:

代码输出结果如下:

上述例子中并没有使用make , 而是使用大括号进行内容定义,就像JSON 格式一样,冒号的左边是key ,右边是值,键值对之间使用逗号分隔。
在编写代码的时候,不要用接口类型作为映射的键的类型
2、遍历map 的”键值对”-访问每一个map 中的关联关系:
map 的遍历过程使用for range 循环完成, 代码如下:

代码输出结果如下:

遍历对于Go 语言的很多对象来说都是差不多的,直接使用for range 语法。遍历时,可以同时获得键和值。如只遍历值,可以使用下面的形式:
for _, v : = range scene {} //将不需要的键改为医名变量形式。
只遍历键时,使用下面的形式:
for k : = range scene {} //无须将值改为匿名变量形式, 忽略值即可
注意:遍历输出元素的顺序与填充顺序无关。不能期望map 在遍历时返回某种期望顺序的结果,如果需要特定顺序的遍历结果,正确的做法是排序
3、使用delete()函数从map 中删除键值对:
使用delete()内建函数从map中删除一组键值对, delete()函数的格式如下:
delete ( map , 键)
- map 为要删除的map 实例
- 键为要删除的map 键值对中的键
例如:通过delete()函数删除下面map中的键,如图:

代码输出结果如下:

4、清空map 中的所有元素:
Go 语言中并没有为map提供任何清空所有元素的函数、方法。清空map的唯一办法就是重新make 一个新的map
5、能够在并发环境中使用的map—–sync.Map
Go 语言中的map 在并发情况下,只读是线程安全的,同时读写线程不安全。
Go 语言在1.9 版本中提供了一种效率较高的并发安全的sync . Map,sync.Map 和map 不同,不是以语言原生形态提供, 而是在sync 包下的特殊结构。
sync.Map 有以下特性:
- 无须初始化, 直接声明即可
- sync.Map 不能使用map 的方式进行取值和设置等操作,而是使用sync . Map 的方法进行调用。Store 表示存储, Load 表示获取, Delete 表示删除
- 使用Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值。Range 参数中的回调函数的返回值功能是: 需要继续迭代遍历时,返回true;终止迭代遍历时,返回false
例如:下面例子展示了如何使用sync.Map,如图:

代码输出结果如下:

注意:sync. Map没有提供获取map 数量的方法,替代方法是获取时遍历自行计算数量。sync.Map为了保证并发安全有一些性能损失,因此在非并发情况下,使用map 相比使用sync.Map会有更好的性能
列表:
列表是一种非连续存储的容器,由多个节点组成,节点通过一些变量记录彼此之间的关系。列表有多种实现方法,如单链表、双链表等
在Go 语言中, 将列表使用container/ list 包来实现,内部的实现原理是双链表。列表能够高效地进行任意位置的元素插入和删除操作
1、初始化列表:
list的初始化有两种方法: New 和声明。两种方法的初始化效果都是一致的。
通过container/list 包的New 方法初始化list,命令如下:
变量名 := list. New()
通过声明初始化list:
var 变量名 list . List
列表与切片和map不同的是,列表并没有具体元素类型的限制,因此,列表的元素可以是任意类型,但是如果给一个列表放入了非期望类型的值,在取出值后,将interface{}转换为期望类型时将会发生岩机。
2、在列表中插入元素:
双链表支持从队列前方或后方插入元素,分别对应的方法是PushFront 和PushB ack
注意:这两个方法都会返回一个*list.Element结构。如果在以后的使用中需要删除插入的元素,则只能通过*list. Element 配合Remove()方法进行删除, 这种方法可以让删除更加效率化, 也是双链表特性之一
下面通过例子展示如何向列表中插入元素,如图:

代码输出结果如下:

3、从列表中删除元素
列表的插入函数的返回值会提供一个*list.Element 结构, 这个结构记录着列表元素的值及和其他节点之间的关系等信息。从列表中删除元素时, 需要用到这个结构进行快速删除。
例如: 通过如下例子展示如何删除列表中的元素,如图:

代码输出结果如下:

4、遍历列表:
遍历双链表需要配合Front() 函数获取头元素, 遍历时只要元素不为空就可以继续进行。每一次遍历调用元素的Next,如图:

注意:使用for 语句进行遍历, 其中i := listone.Front()表示初始赋值,只会在一开始执行一次:每次循环会进行一次i !=nil 语句判断,如果返回false ,表示退出循环,反之则会执行i = i.Next(),最后使用遍历返回的 *list.Element 的Value 成员取得放入列表时的原值。


