前言

哈希表是一种巧妙并且实用的数据结构。它是一个无序的 key/value对 的集合,其中所有的 key 都是不同的,然后通过给定的 key 可以在常数时间复杂度内检索、更新或删除对应的 value。

在 Go 语言中,一个 map 就是一个哈希表的引用,map 类型可以写为 map[K]V,其中 K 和 V 分别对应 key 和 value。map 中所有的 key 都有相同的类型,所有的 value 也有着相同的类型,但是 key 和 value 之间可以是不同的数据类型。其中 K 对应的 key 必须是支持 == 比较运算符的数据类型(切片、函数等不支持),所以 map 可以通过测试 key 是否相等来判断是否已经存在。虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做 key 类型则是一个坏的想法。对于 V 对应的 value 数据类型则没有任何的限制。

  • map 是无序的
  • 在 Go 语言中的 map 是引用类型,必须初始化才能使用

Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。由于 map 是无序的,我们无法决定它的返回顺序。

map 的定义

可以使用内建函数 make 也可以使用 map 关键字来定义 map:

// 使用 make 函数
m := make(map[keyType]valueType)
// 长度为 0 的 map
m := make(map[keyType]valueType, 0)// 声明变量,默认 map 是 nil
var m map[keyType]valueType
// 长度为 0 的 map
var m map[keyType]valueType{}

其中:

  • m 为 map 的变量名。
  • keyType 为键类型。
  • valueType 是键对应的值类型。

在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的。但是如果我们提前知道 map 需要的长度,最好指定一下。

我们可以用 len(m) 来查看 map 的长度。注意,使用 cap(m) 会报错(cap 支持 数组、指向数组的指针、切片、channel):

invalid argument m (type map[string]int) for cap

如果不初始化 map,那么就会创建一个 nil map。nil map 不能用来存放键值对。如果向一个 nil 值的 map 存入元素将导致一个 panic 异常:

下面我们用 make 函数创建一个 map:

ages := make(map[string]int)

当然,我们也可以直接创建一个 map 并且指定一些最初的值:

ages := map[string]int{"Conan": 18,"Kidd": 23,
}

这种就相当于:

ages := make(map[string]int)
ages["Conan"] = 18
ages["Kidd"] = 23

所以,另一种创建空(不是 nil)的 map 方法是:

ages := map[string]int{}

map 在定义时,key 是唯一的,不允许重复(value 可以重复)。下面的程序会报错

ages := map[string]int{"Conan": 18,"Conan": 23,
}

但是之后在对 map 赋值时,则会覆盖原来的 value

ages["Conan"] = 18
ages["Conan"] = 23
fmt.Println(ages["Conan"])  // 23

map 类型的零值是 nil,也就是没有引用任何哈希表,其长度也为 0.

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

map 的基本使用

增加 map 的值很简单,只需要 m[key] = value 即可,比如:

ages := make(map[string]int)
ages["Conan"] = 18
ages["Kidd"] = 23

使用内置的 delete 函数可以删除元素,参数为 map 和其对应的 key,没有返回值:

delete(ages, "Conan")

注意:即使这些 key 不在 map 中也不会报错。

修改 map 的内容和 增 的写法类似,只不过 key 是已存在的,如果不存在,则为增加,例如:

ages := map[string]int{"Conan": 18,"Kidd": 23,
}
ages["Conan"] = 21

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

ages["Conan"] = 18
fmt.Println(ages["Conan"])  // 18

要想遍历 map 中全部的键值对的话,可以使用 range 风格的 for 循环实现,和之前的 slice 遍历语法类似。例如:

for key, value := range ages {fmt.Println(key, value)
}

如果用不到 value,无需使用匿名变量 _,直接不写即可:

for key := range ages {fmt.Println(key)
}

如果查找失败也没有关系,程序也不会报错,而是返回 value 类型对应的零值。例如:

ages := map[string]int{"Conan": 18,"Kidd": 23,
}
fmt.Println(ages["Lan"])  // 0

通过 key 作为索引下标来访问 map 将产生一个 value。如果 key 在 map 中是存在的,那么将得到与 key 对应的 value;如果 key 不存在,那么将得到 value 对应类型的零值。

但是有时候我们需要知道对应的元素是否真的是在 map 之中。比如,如果元素类型是一个数字,你需要区分一个已经存在的 0,和不存在而返回零值的 0。例如:

ages := map[string]int{"Conan": 18,"Kidd": 23,
}
// 如果 key 存在,则 ok = true;不存在,ok = false
if value, ok := ages["Conan"]; ok {fmt.Println(value)
} else {fmt.Println("key 不存在")
}

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

map 的迭代顺序是不确定的。有没有什么办法可以顺序的打印出 map 呢?我们可以借助切片来完成。先将 key(或者 value)添加到一个切片中,再对切片排序,然后使用 for-range 方法打印出所有的 key 和 value。如下所示:

package mainimport ("fmt""sort"
)func main() {// 创建一个 ages map,并给三个值ages := make(map[string]int)ages["Conan"] = 18ages["Kidd"] = 23ages["Lan"] = 19// 创建一个切片用于给 key 进行排序var names []stringfor name := range ages {names = append(names, name)}sort.Strings(names)// 循环打印出 map 中的值for _, name := range names {fmt.Printf("%s\t%d\n", name, ages[name])}
}

因为我们一开始就知道 names 的最终大小,因此给切片分配一个合适的容量大小将会更有效。下面的代码创建了一个空的切片,但是切片的容量刚好可以放下 map 中全部的 key:

names := make([]string, 0, len(ages))

当然,如果使用结构体切片,这样就会更有效:

type name struct {key stringvalue int
}

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

func equalMap(x, y map[string]int) bool {// 长度不一样,肯定不相等if len(x) != len(y) {return false}for k, xv := range x {if yv, ok := y[k]; !ok || xv != yv {return false}}return true
}

map 作为函数参数

map 作为函数参数是地址传递(引用传递),作返回值时也一样。

在函数内部对 map 进行操作,会影响主调函数中实参的值。例如:

func foo(m map[string]int) {m["Conan"] = 22m["Lan"] = 21
}func main() {m := make(map[string]int, 2)m["Conan"] = 18fmt.Println(m)  // map[Conan:18]foo(m)fmt.Println(m)  // map[Conan:22 Lan:21]
}

并发环境中使用的 map:sync.Map

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

下面我们来看一下在并发情况下读写 map 时会出现的问题,代码如下:

// 创建一个 map
m := make(map[int]int)// 开启一个 go 程
go func () {// 不停地对 map 进行写入for true {m[1] = 1}
}()// 开启一个 go 程
go func() {// 不停的对 map 进行读取for true {_ = m[1]}
}()// 运行 10 秒停止
time.Sleep(time.Second * 10)

运行代码会报错,错误如下:

fatal error: concurrent map read and map write

当两个并发函数不断地对 map 进行读和写时,map 内部会对这种并发操作进行检查并提前发现。

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

Go 语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map。

sync.Map 有以下特性:

  • 无须初始化,直接声明即可
  • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用:Store 表示存储,Load 表示获取,Delete 表示删除。
  • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时返回 true,终止迭代遍历时,返回 false。

并发安全的 sync.Map 示例代码如下:

package mainimport ("fmt""sync"
)func main() {var ages sync.Map// 将键值对保存到 sync.Mapages.Store("Conan", 18)ages.Store("Kidd", 23)ages.Store("Lan", 18)// 从 sync.Map 中根据键取值age, ok := ages.Load("Conan")fmt.Println(age, ok)// 根据键删除对应的键值对ages.Delete("Kidd")fmt.Println("删除后的 sync.Map: ", ages)// 遍历所有 sync.Map 中的键值对ages.Range(func(key, value interface{}) bool {fmt.Println(key, value)return true})
}

sync.Map 没有提供获取 map 数量的方法,替代方法是在获取 sync.Map 时遍历自行计算数量,sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。

所以,我们用 sync.Map 时进行同时读写是没问题的,示例代码如下:

package mainimport ("fmt""sync""time"
)func main() {var m sync.Map// 开启一个 go 程go func() {// 不停地对 map 进行写入for true {m.Store(1, 1)}}()// 开启一个 go 程go func() {// 不停的对 map 进行读取并打印读取结果for true {value, _ := m.Load(1)fmt.Println(value)}}()time.Sleep(time.Second * 10)
}

这时的结果就会一直输出 1。

练习

1、封装 wordCountFunc() 函数。接收一段英文字符串 str。返回一个 map,记录 str 中每个“单词”出现的次数。

示例:

输入:"I love my work and I love my family too"
输出:family:1too:1I:2love:2my:2work:1and:1

提示:使用 strings.Fields() 函数可提高效率

实现:

package mainimport ("fmt""strings"
)func wordCountFunc(str string) map[string]int {// 使用 strings.Fields 进行拆分, 自动按照空格对字符串进行拆分成切片wordSlice := strings.Fields(str)// 创建一个用于存储 word 次数的 mapm := make(map[string]int)// 遍历拆分后的字符串切片for _, value := range wordSlice {if _, ok := m[value]; !ok {// key 不存在m[value] = 1} else {// key 值已存在m[value]++}}return m
}func main() {str := "I love my work and I love my family too"res := wordCountFunc(str)// 遍历 map, 展示每个 word 出现的次数for key, value := range res {fmt.Println(key, ": ", value)}
}

如需更深入的了解 map 的原理,推荐阅读这篇文章:深度解密Go语言之map

李培冠博客

欢迎访问我的个人网站:

李培冠博客:lpgit.com

Golang map相关推荐

  1. Golang map 三板斧第二式:注意事项

    文章目录 1.默认初始值为 nil 2.range 顺序的随机性 3.引用传递 4.元素不可取址 5.并发读写问题 参考文献 map 使用起来非常方便,但也有些必须要注意的地方,否则可能会导致程序异常 ...

  2. golang map嵌套struct 结构体字段 不能直接修改 解决方法

    目录 错误信息 错误原因 解决方法 错误信息 Reports assignments directly to a struct field of a map 错误原因 结构体作为map的元素时,不能够 ...

  3. golang map 排序

    golang中map元素是随机无序的,所以在对map range遍历的时候也是随机的,不像php中是按顺序.所以如果想按顺序取map中的值,可以采用以下方式: import ("fmt&qu ...

  4. golang map源码分析

    2019独角兽企业重金招聘Python工程师标准>>> 1. map数据结构 Golang的map使用哈希表作为底层实现,一个哈希表里可以有多个哈希表节点,也即bucket,而每个b ...

  5. Golang map 如何进行删除操作?

    map 的删除操作 Golang 内置了哈希表,总体上是使用哈希链表实现的,如果出现哈希冲突,就把冲突的内容都放到一个链表里面. Golang 还内置了delete函数,如果作用于哈希表,就是把 ma ...

  6. golang map合并_Golang之流式编程

    流处理(Stream processing)是一种计算机编程范式,其允许给定一个数据序列(流处理数据源),一系列数据操作(函数)被应用到流中的每个元素.同时流处理工具可以显著提高程序员的开发效率,允许 ...

  7. Golang map 三板斧第三式:实现原理

    文章目录 1.数据结构 1.1 简介 1.2 核心结构 1.3 数据结构图 2.实现机制 2.1 创建 2.2 增加或修改 2.3 删除 2.4 查找 2.5 迭代 2.5.1 hiter 2.5.2 ...

  8. Golang map 三板斧第一式:快速上手

    文章目录 1.简介 2.申明与定义 3.遍历 4.增删改查 参考文献 1.简介 map 是经常被使用的内置 key-value 型容器,是一个同类型元素的无序组,元素通过另一类型唯一键进行索引. 其键 ...

  9. Golang map的底层实现

    转自https://blog.csdn.net/i6448038/article/details/82057424并修改 map是Go语言中基础的数据结构,在日常的使用中经常被用到.但是它底层是如何实 ...

  10. Golang map源码详解

    Golang的map是用哈希表实现的,在实现性能上非常优秀,这里会主要对map创建.插入.查询.删除以及删除全部的源码做详解,刻意避开了扩容以及迭代相关的代码,后续会用一个新的文章去讲述.Golang ...

最新文章

  1. linux查找项目中的问题,教你如何快速定位项目中慢查询[项目管理]
  2. 俄罗斯机器人雄鹿_世界上最''倒霉''的机器人:逼真到难以置信,为忍受痛苦而生...
  3. 分别是什么意思_美国FBA头程:空派/海派分别是什么意思?
  4. PreparedStatement批量处理的一个Framework(原创)
  5. mybatis动态sql中的where标签的使用
  6. 《Android群英传:神兵利器》— Android 书籍
  7. pandas dataframe统计填充空值大全
  8. 什么是监控报警以及如何实现
  9. MySQL专题系统归纳快速上手(常用cmd命令,常用函数汇总,SQL语句精讲带示例)适用初学、用法速查
  10. DDoS攻击及防御技术综述
  11. Java(1):Java SE疯狂复习基本数据类型、OOP
  12. XML文档定义有几种形式?解析XML文档有哪几种方式?
  13. Python常用模块(一)pandas
  14. 包子笔记 - 三知道原则
  15. 深受程序员鄙视的外行语录!
  16. python图片压缩算法_使用K均值算法进行图片压缩
  17. 魔力宝贝 - 窗口分辨率修改
  18. camstar portal 7全屏显示
  19. Java----jar包
  20. ios解锁动态图片_iPhone不越狱如何设置酷炫的动态锁屏壁纸

热门文章

  1. 短视频剪辑制作教学:编辑短视频时需要注意的三个方面
  2. FPGA专题-相位累加器(DDS)
  3. 技术科普丨马达测试知多少?
  4. 区间素数个数 树状数组 HIT 1867 经理的烦恼
  5. 读取txt的中文字符出现乱码
  6. 少儿编程--scratch编程--游来游去的鱼
  7. matlab中示波器如何使用,[原创]Matlab上位机示波器(中) · 立羽博客
  8. 矩阵的初等变换的应用
  9. python3中使用requests库出现的编码问题
  10. 复合字面量(compound literals)详解