Go从入门到精通——映射(map)——建立事物关联的容器

映射(map)——建立事物关联的容器

  在业务和算法中需要使用任意类型的关联关系时,就需要使用到映射,如学号和学生的对应、名字与档案的对应关系等。

  Go 语言提供的映射关系容器为 map。map 使用散列表(hash)实现。

大多数语言中映射关系容器使用两种算法:散列表和平衡树。
散列表可以简单描述为一个数组(俗称“桶”),数组的每个元素是一个列表。根据散列函数获得每个元素的特征值,将特征值作为映射的键。

  如果特征值重复,表示元素发生碰撞。碰撞的元素将被放在同一个特征值的列表中进行保存。散列表查找复杂度为 O(1),和数组一致。
  最坏的情况为 O(n),n 为元素的总数。散列需要尽量表面元素碰撞以提高查找效率,这样就需要对 "" 进行扩容,每次扩容,元素需要重新放入桶中,较为耗时。 平衡树类似有父子关系的一棵数据树,每个元素在放入树时,都要与一些节点进行比较。平衡树的查找复杂度始终未 O(log n)。

1.1、添加关联到 map 并访问关联和数据

  Go 语言中 map 的定义是这样的:

1
2
3
map[KeyType]ValueType
//KeyType 为键类型
//ValueType 是键对应的值类型

  一个 map 里,符合 KeyType 和 ValueType 的映射总是成对出现。话说这像不像 python 的字典?

  下面举个例子:

1
2
3
4
5
6
7
8
9
package main
 
import "fmt"
 
func main() {
    capital := make(map[string]string)
    capital["江苏"] = "南京"
    fmt.Println(capital["江苏"])
}

  代码说明如下:

  • 第 6 行:map 是一个内部实现的类型,使用时,需要手动使用 make 创建。如果不创建使用 map 类型,会触发宕机错误。
  • 第 7 行:向 map 中加入映射关系。写法与使用数组一样,key 可以使用除函数意外的任意类型。
  • 第 8 行,查找 map 中的值。

  如果在 map 中查找一个不存在的键呢?拿上面的例子改改:

1
2
3
4
5
6
7
8
9
10
11
12
package main
 
import "fmt"
 
func main() {
    capital := make(map[string]string)
    capital["江苏"] = "南京"
    fmt.Println(capital["江苏"])
 
    c := capital["上海"]
    fmt.println(c)
}

  需要知道查询中某个键是否在 map 中存在,可以使用一种特殊的写法来实现,这个也是我们经常在 Go 语言中大量见到:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
 
import "
 
func main() {
    capital := make(map[string]string)
    capital["江苏"] = "南京"
    fmt.Println(capital["江苏"])
 
    if v, ok := capital["江苏"]; ok {
        //存在
    }
}

1.2、遍历 map 的 "键值对"——访问每一个map中的关联关系

  map 的遍历过程使用 for range 循环完成。

1
2
3
for k,v := range capital{
    fmt.Println(k,v)
}

  只遍历 键 ,表现形式如下:

1
2
for k := range capital{
}

  只遍历 值,表现形式如下:

1
2
for _, v := range capital{  //无需将值改为匿名变量形式,忽略值即可。
}

  注意:遍历输出元素的顺序和填充顺序无关。不能期望 map 在遍历时返回某种期望顺序的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main
 
import (
    "fmt"
    "sort"
)
 
func main() {
    scene := make(map[string]int)
 
    //准备 map 数据
    scene["route"] = 66
    scene["brazil"] = 4
    scene["china"] = 960
 
    //声明一个切片保存 map 数据
    var sceneList []string
 
    //将 map 数据遍历复制到切片中
 
    for k := range scene {
        sceneList = append(sceneList, k)
    }
 
    // 对切片进行排序
    sort.Strings(sceneList)
 
    // 输出
    fmt.Println(sceneList)
}

 1.3、使用 delete() 函数从 map 中删除键值对

  使用 delete() 内建函数从 map 中删除一组键值对,delete() 函数的格式如下:

1
2
3
delete(map,键)
//map 为要删除的 map 实例
//键 为要删除的 map 键值对中的键

1.4、清空 map 中的所有元素

  Go 语言中并没有为 map 提供任何清空所有元素的函数、方法。清空 map 的唯一办法就是重新 make 一个新的 map。不用担心垃圾回收的效率,Go 语言中的并行垃圾回收效率比写一个清空函数高效多了。

1.5、能够在并发环境中使用的 map —— sync.Map(适合大量读,少量写)

  Go 语言中的 map 在并发清下,只读是线程安全的,同时读写线程不安全。

  并发情况下读写 map 会出现的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main
 
func main() {
 
    //创建一个 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 内部会对这种并发操作进行检查并提前发现。

  需要并发读写时,一般的做法是加锁,但这样的性能并不高。

  Go 语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map。sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构。

  sync.Map 有以下特性:

  • 无须初始化,直接使用即可。
  • sync.Map 不能使用 map 的方式进行取值和设置操作,而是使用 sync.Map 的方法进行调用。Store 表示存储,Load 表示获取,Delete 表示删除。
  • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值。Range 参数中的回调函数的返回值功能是:需要继续迭代遍历时,返回 true;终止迭代遍历时,返回 false。
  • 原理是通过分离读写 map 和 原子指令 来实现读的近似无锁,并通过延迟更新的方式来保证读的无锁化。
  • 空间换时间:通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。sync.Map 的主要思想就是读写分离,空间换时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Load(key interface{}) (value interface{}, ok bool)
//通过提供一个键key,查找对应的值value,如果不存在,则返回nil。ok的结果表示是否在map中找到值
  
Store(key, value interface{})
//这个相当于是写map(更新或新增),第一个参数是key,第二个参数是value
  
LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
//通过提供一个键key,查找对应的值value,如果存在返回键的现有值,否则存储并返回给定的值,如果是读取则返回true,如果是存储返回false
  
Delete(key interface{})
//通过提供一个键key,删除键对应的值
  
Range(f func(key, value interface{}) bool)
//循环读取map中的值。
//因为for ... range map是内置的语言特性,所以没有办法使用for range遍历sync.Map, 但是可以使用它的Range方法,通过回调的方式遍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main
 
import (
    "fmt"
    "sync"
)
 
func main() {
    var capital sync.Map
 
    //将键值对保存到 sync.Map
    capital.Store("SH", 10)
    capital.Store("BJ", 21)
    capital.Store("NJ", 25)
 
    //从sync.Map中根据键取值
    number, _ := capital.Load("SH")
    fmt.Println(number.(int))
 
    //根据键删除对应的键值对
    capital.Delete("SH")
 
    //遍历所有 sync.Map 中的键值对
    capital.Range(func(k, v interface{}) bool {
 
        fmt.Println("capital:", k, v)
        return true
    })
}

  代码说明如下:

  • 第 9 行,声明 capital,类型为 sync.Map。注意,sync.Map 不能使用 make 创建。
  • 第 11~14 行,将一些列键值对保存到 sync.Map 中,sync.Map 将键和值以 interface{} 类型进行保存。
  • 第 17 行,提供了一个 sync.Map 的键给 scene.Load() 方法后将查询到键对应的值返回。
  • 第 21 行,sync.Map 的 Delete 可以使用指定的键将对应的键值对删除。
  • 第 24 行,Range() 方法开遍历 sync.Map,遍历需要提供一个匿名函数,参数为 k、v,类型为 interface{},每次 Range() 在遍历一个元素时,都会调用这个匿名函数把结果返回。

  sync.Map 没有提供获取 map 数量的方法,替代方法是获取时遍历自行计算数量。

  sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。

posted @   左扬  阅读(214)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
levels of contents
点击右上角即可分享
微信分享提示