Go语言 映射(Map)

映射是一种数据结构,它是一种元素对的无序集合,每一个索引(key)**对应一个值(value)**,这种数据结构在 Go 语言中被称之为 map
映射强大的地方是能够基于键快速检索数据。键就像索引一样,指向与该键关联的值。

内部实现

映射是一个集合,可以使用类似处理数组和切片的方式迭代映射中的元素。但映射是无序的,即使使用同样的顺序保存键对,在每次迭代映射的时候顺序也有可能不一样。因为映射实现使用的是哈希表。
哈希表是什么不用过多介绍了,没了解的给一个传送门再自行了解什么是哈希表
哈希表,百度百科介绍

创建和初始化

使用make关键字
// 使用make关键字
// 创建一个映射,键类型是string,值类型是int
mobile := make(map[string]int)
fmt.Println(mobile) // map[]

// 使用映射字面量
// 创建一个映射,键和值类型都是string
dict := map[string]string{"apple": "iPhone14 Pro", "huawei": "meta40 Pro"}
fmt.Println(dict)  // map[apple:iPhone14 Pro huawei:meta40 Pro]
fmt.Println(dict["apple"]) // iPhone14 Pro 
使用映射字面量

最常用的方法是使用映射字面量。映射的初始长度会根据初始化时指定的键值对的数量来确定。

映射的键不能是切片,函数以及包含切片类型的结构类型,因为这些是具有引用语义,不能作为映射的键。

// 创建一个映射,使用字符串切片作为映射的值
dict2 := map[int][]string{0: {"apple", "huawei"}, 1: {"oppo", "xiaomi"}}
fmt.Println(dict2)
// map[0:[apple huawei] 1:[oppo xiaomi]]  

使用映射(Map)

Map中的元素通过key对应的下标语法访问

map[key]
赋值
map[key] = value

示例:

// 创建一个映射,键类型是string,值类型是int
mobile := make(map[string]int)
// 对映射赋值
mobile["iPhone 14 pro"] = 10200
fmt.Println(mobile)

删除元素

使用 delete 关键字

// 字面量
dict2 := map[string]int{"iPhone": 10090, "huawei": 6888}
fmt.Println(dict2)
// 删除 iPhone 元素
delete(dict2, "iPhone")
delete(dict2, "iPhone2") // 删除一个不存在的元素
fmt.Println("dict2:", dict2)  // dict2: map[huawei:6888]

即使这些元素不在map中也没有关系;如果一个查找失败将返回value类型对应的零值

其它

x += y和x++等简短赋值语法也可以用在map上

// x += y和x++等简短赋值语法示例
dict3 := map[string]int{
	"page":  1,
	"limit": 10,
	"total": 100,
}
dict3["page"] += 1
dict3["page"]++
fmt.Println("dict3:", dict3)

但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作:

// invalid operation: cannot take address of dict3["page"] (map index expression of type int)
fmt.Println("dict3 page address:", &dict3["page"])

禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效

遍历map

语法示例:

pages := map[string]int{
	"page":  1,
	"limit": 10,
	"total": 100,
}
for k, v := range pages {
	fmt.Printf("key: %s, value: %d \n", k, v)
}

Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。
在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。

排序示例

借助 slice 对 key 进行有序排序,再遍历实现一个排序的效果。


import (
	"fmt"
	"sort"
)

// 排序示例
mobile := map[string]int{
	"apple":  1,
	"huawei": 2,
	"oppo":   3,
	"xiaomi": 4,
}
fmt.Println(mobile)
var names []string
for name := range mobile {
	names = append(names, name)
}
// 对 key 的 slice 进行有序排序
sort.Strings(names)
// 这里遍历的是 names 这个有序的 slice,再对 map 进行指定 key 访问。
for _, name := range names {
	fmt.Printf("name: %s, value: %d\n", name, mobile[name])
}

我们在已知 map 长度的情况下,上面的 names 切片可以优化成

names := make([]string, 0, len(mobile))
map的 nil 值

map类型的零值是nil,也就是没有引用任何哈希表。

  • 查找、删除、len和range循环都可以安全工作在nil值的map上
  • nil值的map存入元素将导致一个panic异常
dict1 := map[string]int{}
fmt.Println(dict1 == nil)    // false,这里是空值,不是 nil
fmt.Println(len(dict1) == 0) // true

var dict2 map[string]int
fmt.Println(dict2 == nil)    // true
fmt.Println(len(dict2) == 0) // true

// nil 值可以循环遍历,查找,删除,len
for k, v := range dict1 {
	fmt.Printf("key: %s, value: %d\n", k, v)
}

for k, v := range dict2 {
	fmt.Printf("key: %s, value: %d\n", k, v)
}

// 向一个 nil 值的 map 存入一个值将会异常
dict2["age"] = 18  // panic: assignment to entry in nil map
dict1["name"] = 18 // dict1正常,因为 dict1不是 nil 而是空值

在向map存数据前必须先创建map(非 nil)
通过 key 作为索引下标来访问 map 将产生一个 value 值。

  • 如果 key 在 map 中是存在的则得到一个 key 对应的 value
  • 如果 key 不存在,刚得到 value对应类型的 nil 值
    如果类型是数字,区分0值和 nil 值
// 访问 dict2中不存在的一个 key 值
_, ok := dict2["nokey"]
if !ok {
	fmt.Println("error:", ok) // error: false
}

在这种场景下,map的下标语法将产生两个值;第二个是一个布尔值,用于报告元素是否真的存在。布尔变量一般命名为ok,特别适合马上用于if条件判断部分。

和slice一样,map之间不能进行相等比较;
唯一的例外是和nil进行比较。
要判断两个map是否包含相同的key和value,我们必须通过一个循环实现:

// 判断两个map是否包含相同的key和value
func equal(x, y map[string]int) bool {
	// 先判断两个 map 的长度
	if len(x) != len(y) {
		return false
	}
	// 遍历map x
	for k, xv := range x {
		// 从 y中取 x 的 key 值,如果值不存在或者y中取的 key 值的 value 不一致,即为 false
		if yv, ok := y[k]; !ok || yv != xv {
			return false
		}
	}
	return true
}

func map4() {
	map1 := make(map[string]int)
	map1["age"] = 19
	map1["abc"] = 20

	map2 := map[string]int{"age": 19, "abc": 20}
	flag := equal(map1, map2)
	fmt.Println("equal 比对结果:", flag)
}

func main() {
	map4()
}
去重,模拟 set

Go语言中并没有提供一个set类型,可以用map实现类似set的功能。

// 统计字符出现的次数
str := "滚滚长江东逝水,浪花淘尽英雄。"
count := map[string]int{}
for _, v := range str {
	if _, ok := count[string(v)]; ok {
		count[string(v)] += 1
	} else {
		count[string(v)] = 1
	}
}

// 按照字符出现次数的多少排序
// 定义一个结构体
type strInfo struct {
	s string
	n int
}

var lenInfo []strInfo
for k, v := range count {
	lenInfo = append(lenInfo, strInfo{k, v})
}
// sort 的排序方法
sort.Slice(lenInfo, func(i, j int) bool {
	return lenInfo[i].n < lenInfo[j].n // 升序
})
fmt.Println(lenInfo)

锁机制

无锁的map

这种类型的 map 是线程不安全的 map,多个线程同时访问这个类型的 map 的同一个变量时,会有读写冲突,会导致系统奔溃。
所以一般在单线程程序中使用的较多。

自带锁的 sync.Map

这种类型的 map 是线程安全的 map,多个线程同时访问这个类型的 map 的同一个变量时,不会有读写冲突,因为它自带原子锁,保障了多线程的数据安全。

sync.Map 的创建

这种类型的 map 创建不需要make,直接声明就可以使用,而且不需要声明 map 的 key 和 value 的类型。
因为它底层的实现并不是指针,是一种多个变量的聚合类型,叫做结构体

package map_test

import (
	"sync"
	"testing"
)

func TestMapDemo(t *testing.T) {
	var m sync.Map
	t.Log(m) // 零值 nil

}

sync.Map 的操作

package map_test

import (
	"sync"
	"testing"
)

func TestMapDemo(t *testing.T) {
	var m sync.Map
	t.Log(m) // {{0 0} {<nil>} map[] 0}

	m.Store("Apple", "苹果")
	m.Store("name", "荔枝")
	m.Store("age", 18
	t.Log(m) // {{0 0} {{map[] true}} map[Apple0xc0000a8028 name:0xc0000a8030] 0}
	tmp, exist := m.Load("Apple")
	t.Log(tmp, exist) // 苹果 true

	m.Delete("name")
	m.Range(func(k, v interface{}) bool {
		t.Log("key", k, ",value:", v)
		return true
	})
}
  • 使用 Store 方法给 m 赋值;
  • 使用 Load 取出 “Apple” 对应的值,如果不存在 “Apple” 这个 key,exist 的值为 false;
  • 删除 m 中的 “name” 和其对应的 value;
  • 使用 Range 方法遍历 m。