文章

数组,切片,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

给数组赋值:

  1. 初始化的时候赋值:
	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]
  1. 通过索引下标赋值:
	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

取值

  1. 通过索引下标取值:
	array := [4][2]int{
		{10, 11},
		{20, 21},
		{30, 31},
		{40, 41}}
	fmt.Println(array[1][1]) // 21
  1. 循环取值:
	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]
  1. 赋值:
	// 声明一个 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 会有更好的性能。

License:  CC BY 4.0 test