哈希表是一种巧妙而且实用的数据结构。它是一个无序的 key/value对 的集合,其中全部的 key 都是不一样的,而后经过给定的 key 能够在常数时间复杂度内检索、更新或删除对应的 value。html
在 Go 语言中,一个 map 就是一个哈希表的引用,map 类型能够写为 map[K]V,其中 K 和 V 分别对应 key 和 value。map 中全部的 key 都有相同的类型,全部的 value 也有着相同的类型,可是 key 和 value 之间能够是不一样的数据类型。其中 K 对应的 key 必须是支持 == 比较运算符的数据类型(切片、函数等不支持),因此 map 能够经过测试 key 是否相等来判断是否已经存在。虽然浮点数类型也是支持相等运算符比较的,可是将浮点数用作 key 类型则是一个坏的想法。对于 V 对应的 value 数据类型则没有任何的限制。git
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{}
其中:数据结构
在声明的时候不须要知道 map 的长度,由于 map 是能够动态增加的。可是若是咱们提早知道 map 须要的长度,最好指定一下。并发
咱们能够用 len(m)
来查看 map 的长度。注意,使用 cap(m)
会报错(cap 支持 数组、指向数组的指针、切片、channel):app
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 的值很简单,只须要 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 main import ( "fmt" "sort" ) func main() { // 建立一个 ages map,并给三个值 ages := make(map[string]int) ages["Conan"] = 18 ages["Kidd"] = 23 ages["Lan"] = 19 // 建立一个切片用于给 key 进行排序 var names []string for 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 string value 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 进行操做,会影响主调函数中实参的值。例如:
func foo(m map[string]int) { m["Conan"] = 22 m["Lan"] = 21 } func main() { m := make(map[string]int, 2) m["Conan"] = 18 fmt.Println(m) // map[Conan:18] foo(m) fmt.Println(m) // map[Conan:22 Lan:21] }
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 示例代码以下:
package main import ( "fmt" "sync" ) func main() { var ages sync.Map // 将键值对保存到 sync.Map ages.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 main import ( "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。
一、封装 wordCountFunc() 函数。接收一段英文字符串 str。返回一个 map,记录 str 中每一个“单词”出现的次数。
示例:
输入:"I love my work and I love my family too" 输出: family:1 too:1 I:2 love:2 my:2 work:1 and:1
提示:使用 strings.Fields() 函数可提升效率
实现:
package main import ( "fmt" "strings" ) func wordCountFunc(str string) map[string]int { // 使用 strings.Fields 进行拆分, 自动按照空格对字符串进行拆分红切片 wordSlice := strings.Fields(str) // 建立一个用于存储 word 次数的 map m := 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