数组,切片,map
1.数组
Go语言数组的声明:
var 数组变量名 [元素数量]Type
- 数组变量名:数组声明及使用时的变量名。
- 元素数量:数组的元素数量,可以是一个表达式,但最终通过编译期计算的结果必须是整型数值,元素数量不能含有到运行时才能确认大小的数值。
- Type:可以是任意基本类型,包括数组本身,类型为数组本身时,可以实现多维数组。
var arr [3]int
fmt.Printf("值:%d,类型:%T\n", arr, arr) // 值:[0 0 0],类型:[3]int
从数组中取值:
arr := [3]int{1, 2, 3}
fmt.Println(arr[0]) // 1
fmt.Println(arr[1]) // 2
使用for range取值:
arr := [3]int{1, 2, 3}
for index, value := range arr {
fmt.Printf("索引:%d,值:%d \n", index, value)
}
//索引:0,值:1
//索引:1,值:2
//索引:2,值:3
给数组赋值:
- 初始化的时候赋值:
var arr = [3]int{1, 2, 3}
var arr2 = [3]int{1, 2}
var arr3 = [4]int{1, 2, 3, 4}
arr4 := [3]int{1, 2, 3}
arr5 := [...]int{1, 2, 3, 4}
fmt.Println(arr) // [1 2 3]
fmt.Println(arr2) // [1 2 0]
fmt.Println(arr3) // [1 2 3 4]
fmt.Println(arr4) // [1 2 3]
fmt.Println(arr5) // [1 2 3 4]
- 通过索引下标赋值:
var arr [3]int
fmt.Printf("%d\n", arr) // [0 0 0]
arr[0] = 1
arr[1] = 2
fmt.Printf("%d\n", arr) // [1 2 0]
⚠️ 数组是定长的,不可更改,在编译阶段就决定了
给数组定义新的类型:
type arr3 [3]int
var arr arr3
arr[0] = 1
for index, value := range arr {
fmt.Printf("索引:%d,值:%d \n", index, value)
}
//索引:0,值:1
//索引:1,值:0
//索引:2,值:0
给定下标初始状态:
arr := [3]int{2: 3}
for index, value := range arr {
fmt.Printf("索引:%d,值:%d \n", index, value)
}
//索引:0,值:0
//索引:1,值:0
//索引:2,值:3
数组比较:
两个数组类型相同(包括数组的长度,数组中元素的类型),可以直接通过较运算符(
==
和!=
)来判断两个数组是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // true false false
d := [3]int{1, 2}
fmt.Println(a == d)// 编译错误:无法比较 [2]int == [3]int
2. 多维数组
Go语言中允许使用多维数组,因为数组属于值类型,所以多维数组的所有维度都会在创建时自动初始化零值,多维数组尤其适合管理具有父子关系或者与坐标系相关联的数据
声明多维数组的语法:
var array_name [size1][size2]...[sizen] array_type
// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
fmt.Printf("%d %T\n", array, array) // [[10 11] [20 21] [30 31] [40 41]] [4][2]int
// 声明并初始化数组中索引为 1 和 3 的元素
array = [4][2]int{1: {20, 21}, 3: {40, 41}}
fmt.Printf("%d %T\n", array, array) // [[0 0] [20 21] [0 0] [40 41]] [4][2]int
// 声明并初始化数组中指定的元素
array = [4][2]int{1: {0: 20}, 3: {1: 41}}
fmt.Printf("%d %T\n", array, array) // [[0 0] [20 0] [0 0] [0 41]] [4][2]int
取值
- 通过索引下标取值:
array := [4][2]int{
{10, 11},
{20, 21},
{30, 31},
{40, 41}}
fmt.Println(array[1][1]) // 21
- 循环取值:
array := [4][2]int{
{10, 11},
{20, 21},
{30, 31},
{40, 41}}
for index, value := range array {
fmt.Printf("索引:%d,值:%d \n", index, value)
}
// 索引:0,值:[10 11]
// 索引:1,值:[20 21]
// 索引:2,值:[30 31]
// 索引:3,值:[40 41]
- 赋值:
// 声明一个 2×2 的二维整型数组
var array [2][2]int
// 设置每个元素的整型值
array[0][0] = 10
array[0][1] = 20
array[1][0] = 30
array[1][1] = 40
fmt.Println(array) // [[10 20] [30 40]]
只要类型一致,就可以将多维数组互相赋值
// 声明两个二维整型数组 [2]int [2]int
var array1 [2][2]int
var array2 [2][2]int
// 为array2的每个元素赋值
array2[0][0] = 10
array2[0][1] = 20
array2[1][0] = 30
array2[1][1] = 40
// 将 array2 的值复制给 array1
array1 = array2
fmt.Printf("地址:%p,值:%d\n", &array1, array1) // 地址:0x140000200a0,值:[[10 20] [30 40]]
fmt.Printf("地址:%p,值:%d", &array2, array2) // 地址:0x140000200a0,值:[[10 20] [30 40]]
3. 切片
切片(slice)
是对数组的一个连续片段的引用,所以切片是一个引用类型。
这个片段可以是整个数组
,也可以是由起始和终止索引标识的一些项的子集
,需要注意的是,终止索引标识的项
不包括在切片内(左闭右开的区间)。
Go语言中切片的内部结构包含地址
、大小
和容量
,切片一般用于快速地操作一块数据集合。
从连续内存区域生成切片是常见的操作,格式如下:
slice [开始位置 : 结束位置]
理解为数学中的左闭右开 [)
按照下标取值,左边的可以取到,右边的取不到
语法说明如下:
- slice:表示目标切片对象;
- 开始位置:对应目标切片对象的索引;
- 结束位置:对应目标切片的结束索引。
从数组生成切片,代码如下:
var a = [3]int{1, 2, 3}
//a[1:2] 生成了一个新的切片
b := a[1:2]
fmt.Printf("地址:%p,值:%d\n", &a, a) // 地址:0x140000240c0,值:[1 2 3]
fmt.Printf("地址:%p,值:%d", &b, b) // 地址:0x1400000c048,值:[2]
从数组或切片生成新的切片拥有如下特性:
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Printf("地址:%p,值:%d,长度:%d,容量:%d\n", &arr, arr, len(arr), cap(arr)) // 地址:0x14000130000,值:[1 2 3 4 5 6 7 8 9 10],长度:10,容量:10
arr1 := arr[:2]
fmt.Printf("地址:%p,值:%d,长度:%d,容量:%d\n", &arr1, arr1, len(arr1), cap(arr1)) // 地址:0x1400000c048,值:[1 2],长度:2,容量:10
arr2 := arr[1:2]
fmt.Printf("地址:%p,值:%d,长度:%d,容量:%d\n", &arr2, arr2, len(arr2), cap(arr2)) // 地址:0x1400000c078,值:[2],长度:1,容量:9
arr3 := arr[2:]
fmt.Printf("地址:%p,值:%d,长度:%d,容量:%d\n", &arr3, arr3, len(arr3), cap(arr3)) // 地址:0x1400000c0a8,值:[3 4 5 6 7 8 9 10],长度:8,容量:8
arr4 := arr[:]
fmt.Printf("地址:%p,值:%d,长度:%d,容量:%d\n", &arr4, arr4, len(arr4), cap(arr4)) // 地址:0x1400011a0c0,值:[1 2 3 4 5 6 7 8 9 10],长度:10,容量:10
3.1 直接声明新的切片
切片类型声明格式如下:
//name 表示切片的变量名,Type 表示切片对应的元素类型。
var name []Type
// 声明字符串切片
var strList []string
// 声明整型切片
var numList []int
// 声明一个空切片
var numListEmpty = []int{}
// 输出3个切片
fmt.Println(strList, numList, numListEmpty) // [] [] []
// 输出3个切片大小
fmt.Println(len(strList), len(numList), len(numListEmpty)) // 0 0 0
// 切片判定空的结果
fmt.Println(strList == nil) // true
fmt.Println(numList == nil) // true
fmt.Println(numListEmpty == nil) // false
切片是动态结构,只能与 nil 判定相等,不能互相判定相等。声明新的切片后,可以使用 append() 函数向切片中添加元素。
var strList []string
// 追加一个元素
fmt.Printf("地址:%p,值:%v\n", &strList, strList) // 地址:0x1400000c048,值:[]
strList = append(strList, "1")
fmt.Printf("地址:%p,值:%v\n", &strList, strList) // 地址:0x1400000c048,值:[1]
删除:
var strList = []int{1, 2, 3, 4, 5}
// 删除3
arr2 := append(strList[:2], strList[3:]...)
fmt.Println(arr2) // [1 2 4 5]
3.2 使用 make() 函数构造切片
如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:
make( []Type, size, cap )
Type
是指切片的元素类型,size
指的是为这个类型分配多少个元素,cap
为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题
。
a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Printf("地址:%p,值:%d,长度:%d,容量:%d\n", &a, a, len(a), cap(a)) // 地址:0x1400011c030,值:[0 0],长度:2,容量:2
fmt.Printf("地址:%p,值:%d,长度:%d,容量:%d\n", &b, b, len(b), cap(b)) // 地址:0x1400011c048,值:[0 0],长度:2,容量:10
使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。
4. 切片复制
内置函数 copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制
copy() 函数的使用格式如下:
copy( destSlice, srcSlice []T) int
其中 srcSlice
为数据来源切片,destSlice
为复制的目标(也就是将 srcSlice 复制到 destSlice),目标切片必须分配过空间且足够承载复制的元素个数
,并且来源和目标的类型必须一致
,copy() 函数的返回值表示实际发生复制的元素个数。
演示使用 copy() 函数将一个切片复制到另一个切片的过程:
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{6, 7, 8}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
fmt.Println(slice1) // [1 2 3 4 5]
fmt.Println(slice2) // [1 2 3]
使用append进行追加:
arr := []int{1, 2, 3, 4, 5}
arr = append(arr, 6, 7, 8)
fmt.Println(arr) // [1 2 3 4 5 6 7 8]
5. map
map 是一种无序的键值对
的集合。
map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。
map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,map 是无序的,我们无法决定它的返回顺序,这是因为 map 是使用 hash 表来实现的。
map 是引用类型,可以使用如下方式声明:
//[keytype] 和 valuetype 之间允许有空格。
var mapname map[keytype]valuetype
其中:
- mapname 为 map 的变量名。
- keytype 为键类型。
- valuetype 是键对应的值类型。
在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 键值对的数目。
var mapLit map[string]int
var mapAssigned map[string]int
mapLit = map[string]int{"one": 1, "two": 2}
mapAssigned = mapLit
//mapAssigned 是 mapList 的引用,对 mapAssigned 的修改也会影响到 mapList 的值。
mapAssigned["two"] = 3
fmt.Printf("one is: %d\n", mapLit["one"])
fmt.Printf("two is: %d\n", mapLit["two"])
fmt.Printf("ten is: %d\n", mapLit["ten"])
//one is: 1
//two is: 3
//ten is: 0
map的另外一种创建方式:
make(map[keytype]valuetype)
切记不要使用new创建map,否则会得到一个空引用的指针
map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity,格式如下:
make(map[keytype]valuetype, cap)
当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。
5.1 遍历map
// 创建一个简单的map
myMap := map[string]int{
"Alice": 25,
"Bob": 30,
"Charlie": 35,
}
// 使用range遍历map
for key, value := range myMap {
fmt.Printf("Key: %s, Value: %d\n", key, value)
}
//Key: Alice, Value: 25
//Key: Bob, Value: 30
//Key: Charlie, Value: 35
注意:map是无序的,不要期望 map 在遍历时返回某种期望顺序的结果
有序输出:
// 创建一个简单的map
myMap := map[string]int{
"Alice": 25,
"Charlie": 35,
"Bob": 30,
}
// 获取map的所有键,并排序
var keys []string
for key := range myMap {
keys = append(keys, key)
}
sort.Strings(keys)
// 遍历排序后的键,按顺序输出
for _, key := range keys {
value := myMap[key]
fmt.Printf("Key: %s, Value: %d\n", key, value)
}
5.2 删除
使用 delete() 内建函数从 map 中删除一组键值对,delete() 函数的格式如下:
delete(map, 键)
map 为要删除的 map 实例,键为要删除的 map 中键值对的键:
myMap := map[string]int{
"Alice": 25,
"Charlie": 35,
"Bob": 30,
}
fmt.Println(myMap) // map[Alice:25 Bob:30 Charlie:35]
delete(myMap, "Alice")
fmt.Println(myMap) // map[Bob:30 Charlie:35]
Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map,不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。
注意map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。
5.3 线程安全的map
并发情况下读写 map 时会出现问题,代码如下:
// 创建一个int到int的映射
m := make(map[int]int)
// 开启一段并发代码
go func() {
// 不停地对map进行写入
for {
m[1] = 1
}
}()
// 开启一段并发代码
go func() {
// 不停地对map进行读取
for {
_ = m[1]
}
}()
// 无限循环, 让并发程序在后台执行
for {
}
报错❗️:fatal error: concurrent map read and map write
错误信息显示,并发的 map 读和 map 写,也就是说使用了两个并发函数不断地对 map 进行读和写而发生了竞态问题,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 不能使用 make 创建
var scene sync.Map
// 将键值对保存到sync.Map
//sync.Map 将键和值以 interface{} 类型进行保存。
scene.Store("greece", 97)
scene.Store("london", 100)
scene.Store("egypt", 200)
// 从sync.Map中根据键取值
fmt.Println(scene.Load("london"))
// 根据键删除对应的键值对
scene.Delete("london")
// 遍历所有sync.Map中的键值对
//遍历需要提供一个匿名函数,参数为 k、v,类型为 interface{},每次 Range() 在遍历一个元素时,都会调用这个匿名函数把结果返回。
scene.Range(func(k, v interface{}) bool {
fmt.Println("iterate:", k, v)
return true
})
sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。