专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
OSC开源社区  ·  升级到Svelte ... ·  3 天前  
程序猿  ·  “我真的受够了Ubuntu!” ·  2 天前  
程序员的那些事  ·  惊!小偷“零元购”后竟向 DeepSeek ... ·  2 天前  
程序员小灰  ·  DeepSeek做AI代写,彻底爆了! ·  3 天前  
51好读  ›  专栏  ›  SegmentFault思否

go 语言映射(map)要点总结

SegmentFault思否  · 公众号  · 程序员  · 2020-02-08 10:00

正文

本文转载于 SegmentFault 社区

作者:lioney




No.1

定义



•  Go语言中映射是一种字典类型的数据结构,类似于 c++ 和 java 中的 hashmap,用于存储一系列无序的键值对。
映射是基于键来存储值。映射的优势是能够基于键快速索引数据。键就像索引一样,指向与该键关联的值,在内存中键值对的关系如下图所示。

和切片类似,映射维护的是底层数组的指针,属于引用类型。


No.2

内部实现



映射是一个集合,可以使用类似处理数组和切片的方式迭代映射中的元素。但映射是无序的,不能对键值对进行排序。即使按顺序保存,每次迭代的时候顺序也不一样。无序的原因是映射的实现使用了散列表。
这个散列表是由 hmap 实现的,hmap 中维护着存储键值对的 bucket 数组,hmap 和 bucket 数组的关系如下图所示。


bucket 是一个链表结构,每一个 bucket 后面连接的 bucket 表示 bucket 容量不够用时进行扩容添加新的 bucket,bucket 的数据结构如下图所示。

说到散列表,一定要有散列函数,散列函数是散列表中存取元素的关键。Go 语言的映射中也有这样的散列函数 (或叫哈希函数) ,但是 Go 语言把散列函数计算出的散列值可以说是用到了极致。它把散列值分成了高 8 位和低 8 位两部分,如下图所示。

散列值的作用

低 8 位散列值:用于寻找当前 key 位于哪个 bucket;
高 8 位散列值:用于寻找当前 key 位于 bucket 中的哪个位置。

映射的存取过程:在存储,删除或者查找键值对的时候,都要先将指定的键传给散列函数得到一个散列值,然后根据散列值的低 8 位选中对应的桶,最终再根据桶中的索引 (散列值的高 8 位) 将键值对分布到这个桶里。随着映射中存储的增加,索引分布越均匀,访问键值对的速度就越快。因此,映射通过设置合理数量的桶平衡键值对的分布。整个过程如下图所示。

键值对的存储方式:键值对的存储不是以 key1,value1,key2,value2 这样的形式存储,主要是为了在 key 和 value 所占字节长度不同的时候,可以消除 padding 带来的空间浪费。
映射的扩容:当散列表的容量需要增长的时候,Go 语言会将 bucket 数组的容量扩充一倍,产生新的 bucket 数组,并将旧数据迁移到新数组。
判断是否扩充的条件,就是散列表的加载因子,加载因子是一个阈值,表示散列表的空间利用率,Go 语言 map 中的加载因子计算公式为:map 长度 /2^B,阈值是 6.5,B 表示已扩容的次数。
映射中数据的删除

如果 key 是指针类型的,直接将其置空,等待 GC 清除;
如果是值类型的,则清除相关内存;
对 value 做同样的操作;
把 key 对应的索引置为空。


No.3

映射的创建



(1) 使用字面量创建


// 创建一个键为字符串类型,值为整形的map
m1 := map[string]int{"last":2019, "now":2020}
// 获取map中的元素
fmt.Println(m1["last"]) // 2019
fmt.Println(m1["now"]) // 2020

// 使用字面量创建一个空map
m2 := map[string]string{}
fmt.Println(m2) // map[]
映射的键的类型可以是内置类型,也可以是结构类型,但这个类型必须可以使用==运算符做比较。切片,函数以及包含切片的结构类型由于具有引用语义,不能作为映射的键,否则会造成编译错误。

(2) 使用内置的 make 函数来创建

m1 := make(map[int] string) // map的容量使用默认值
m1[1] = "lioney"
m2 := make(map[int] string, 10) // map的容量使用给定值
m2[1] = "carlos"
fmt.Println(m1[1]) // lioney
fmt.Println(m2[1]) // carlos


No.4

映射支持的操作



•  map 中单个键值的访问格式为 mapName[key],可以用于获取或更新 map 中的元素
可以使用 for range 遍历 map 中的元素,不保证每次迭代顺序一致
删除 map 中的某个键值对,使用语法 delete(mapName, key)
使用内置函数 len() 获取 map 中保存的键值对数量,map 中不支持 cap() 函数
    
package main

import "fmt"

func main() {
// 创建一个空的map
m1 := make(map[int] string)
// 向map中添加元素
m1[1] = "lioney"
m1[2] = "carlos"
m1[3] = "tom"
// 从map中删除键为3的元素
delete(m1, 3)
// len()表示map中键值对的数量
fmt.Println("len=", len(m1))
// 遍历map
for k, v := range m1 {
fmt.Println("key=", k, "value=", v)
}
}
上述代码编译后运行结果如下:

len= 2
key= 2 value= carlos
key= 1 value= lioney

Process finished with exit code 0


No.5

映射的使用要点



(1) 对 nil 映射赋值会产生运行时错误

和切片类似,映射在使用时必须对其底层数组进行初始化,要么使用 make 进行初始化,要么使用字面量初始化,如果只是简单地声明了一个 map,而没有进行初始化,就是 nil 映射,是不能对其赋值的,请看下面代码:

// 声明一个map
var colors map[string]string

// 将red加入colors
colors["red"] = "#da137" // panic: assignment to entry in nil map

可以做如下修改

// 声明一个map
var colors map[string]string
// 对map进行初始化
colors = make(map[string]string)

// 将red加入colors
colors["red"] = "#da137" // no panic or error

也可以做如下修改:

// 使用字面量创建要给空map
colors := map[string]string{}

// 将red加入colors
colors["red"] = "#da137" // no panic or error
强烈推荐使用第二种,因为用字面量创建 map 比较简洁而且比较快


(2) 从映射获取值并判断键是否存在


在 Go 语言里,通过键来索引值时,即便这个键不存在也会返回一个值,有时候我们需要判断获取到的值是否时默认的零值,代码如下所示。

// 使用字面量创建一个空map






请到「今天看啥」查看全文