计算机科学中最有用的数据结构之一是哈希表。 存在许多具有不同属性的哈希表实现,但通常它们提供快速查找,添加和删除。 Go提供了一个实现哈希表的内置Map类型。
1. 结构
map[KeyType]ValueType
其中:
KeyType的值必须是任何可比较的类型,即可以用==
比较的值
2. 声明和初始化
map是引用类型,类似的类型有切片和指针等,因此,可以通过声明一个未初始化的map来创建一个值为nill的map
var m map[string]int
nill map不能用于存储键值对,否则会引起panic
m = make(map[string]int, len)
其中,cap为选填值,该map类型数据的容量,超出容量会自动扩容
3. 映射散列表
go语言中的map实际上是一个哈希表,map中的数据被分散于一组桶(buckets)中,每个桶最多存储8个键值对,如果多于8个,则申请一个新的bucket,并将它与之前的bucket连接起来,形成链表。
因此hmap的整体结构如下图所示
对map进行存储,查找,删除等操作,就是对对应桶中的数据进行相应操作。go语言内置一个散列函数,用于将指定map的key转化为一个散列值(hash),散列值的低位用于标记该键值存储的桶(bucket)的标号,高位则用于标记对应桶中key的位置。
3.1 散列函数的工作流程:
3.2 go语言中的map结构:
3.3 查找指定key对应的值:
4.增量扩容
如图3.3所示的哈希表增长时,go语言会将桶(bucket)数组的数量扩充一倍,产生一个新的bucket数组,并将旧数组中的数据迁移至新数组。
扩容是指添加一个bucket_x,每个bucket_x数组的容量为2个。
所以,map的key是无须的:
- 在map扩容的过程中,原来在同一个桶中的key有可能被搬移到不同的桶中
- 在遍历map时,并不是固定地从0号桶开始的,遍历起始桶是一个随机值。
另外一个引申点是:无法对map的key或value取地址
- 原因还是在于map的存在扩容过程,map中的key和value在内存中的位置不是固定的,因此对map的key或value进行取地址操作会触发panic
5. 并发
map对于并发使用是不安全的,因此,当需要从同时执行的goroutine中读写map时,需要加入读写锁控制sync.RWMytex。
这是因为对map的查找、赋值、遍历、删除的过程中都会检测写标志,一旦发现写标志置位(等于1),则直接 panic。 另外sync.Map是线程安全的,可以全局使用。
例如:
var counter = struct{
sync.RWMutex
m map[string]int
}{m: make(map[string]int)}
读
counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)
写
counter.Lock()
counter.m["some_key"]++
counter.Unlock()
6.参考
go in action
https://github.com/tiancaiamao/go-internals/blob/master/zh/02.3.md
https://i6448038.github.io/2018/08/26/map-secret/