当前位置: 代码网 > it编程>前端脚本>Golang > Golang Map简介以及底层原理

Golang Map简介以及底层原理

2024年11月03日 Golang 我要评论
map 简介在go语言中提供了map数据结构来存储键值对数据。map的数据类型为map[k]v,其中k为键的类型,v为值的类型。map的键类型必须支持==操作符,用来比较两个键是否相等。go语言提供了

map 简介

在go语言中提供了map数据结构来存储键值对数据。map的数据类型为map[k]v,其中k为键的类型,v为值的类型。map的键类型必须支持==操作符,用来比较两个键是否相等。go语言提供了4种内置的map操作: lendeletecomparisonassign

map 定义

map_var := make(map[k]v) // 用make函数创建一个空的map,其中k和v分别为键和值的类型
map_var[key] = value // 向map中添加一个键值对
value := map_var[key] // 获取指定键的值
delete(map_var, key) // 从map中删除指定的键及其对应的值

map iteration

go语言提供了两个方法来遍历map中的所有键值对,分别是range方法和len()方法。

// 使用range循环遍历map中的所有键值对
for key, value := range map_var {
// todo ...
}
// 计算map中的元素数量
if len(map_var) > 0 {
// todo ...
}

map 的线程安全

在go语言中,map是非线程安全的,在多线程并发访问时可能导致程序报错。当map被多个协程同时访问时,我们需要使用sync包中的sync.mutex来确保操作的原子性和并发安全。

import "sync"
type safemap struct {
mu sync.mutex
m map[string]int
}
func (sm *safemap) get(key string) int {
sm.mu.lock()
defer sm.mu.unlock()
return sm.m[key]
}
func (sm *safemap) set(key string, value int) {
sm.mu.lock()
defer sm.mu.unlock()
sm.m[key] = value
}
func (sm *safemap) delete(key string) {
sm.mu.lock()
defer sm.mu.unlock()
delete(sm.m, key)
}

map 底层原理

go语言的map在设计上是一种哈希表的数据结构。它利用哈希函数将键映射到不同的存储空间,从而实现高效的查找和插入操作。

哈希函数

哈希函数将字符串映射到一个整数上,这称为哈希值。不同的字符串可能会有相同的哈希值,但相同的字符串必定具有相同的哈希值。哈希函数需要满足两点:

  • 哈希函数的计算结果必须是非负整数,因为负数无法在数组中表示。

  • 两个不同字符串的哈希值尽量不要相等,这样可以避免在查找时产生冲突。

在go语言中,字符串的哈希函数采用的是fnv-1哈希算法,算法代码如下:

const (
offset64 = 14695981039346656037
prime64 = 1099511628211
)
func stringhash(s string) uint64 {
h := uint64(offset64)
for i := 0; i < len(s); i++ {
h ^= uint64(s[i])
h *= prime64
}
return h
}

哈希冲突

在哈希表中,哈希值相同的多个字符串可能会存储在同一个位置上,这种现象叫做哈希冲突。哈希冲突处理策略有开放寻址法、再哈希法和链地址法。

  • 开放寻址法:将发生冲突的条目逐个检索新的空棑直到找到一个空位置来存储当前键值对

  • 再哈希法:对于发生冲突的键,用另一个不同的哈希函数计算地址

  • 链地址法:对于发生冲突的键,将其存储在一个链表中

go语言使用链地址法处理哈希冲突。对于每个存储单元,map结构体中还维护了一个[]keyvalue类型的链表。

type hmap struct {
count int // 映射中的键值对数量
flags uint8 // 控制哈希表的一些属性
b uint8 // 用于计算哈希地址的初始大小
noverflow uint16 // 链表上的溢出桶的数量
}

growing

在go语言中,动态数组会自动地为map分配更多的空间。growing过程涉及到将原始的数组重新复制到一个更大的数组中,其中原数组的元素需要重新计算其在新数组中的位置,而新数组的元素则需要将其键值对填充到相应的位置。growing的过程比较复杂,可以由函数hashgrow()来控制。

// hashgrow() 将map的数组的大小翻倍,并处理哈希冲突。
func hashgrow(h *hmap) {
// ...
buf := make([]keyvalue, newcap)
//...
for i := uintptr(0); i < cap; i++ {
// ...
evacuate(h, &h.oldbuckets[i], &buf)
// ...
}
// ...
}
// evacuate() 将一个bucket中的键值对重新映射到新的数组中
func evacuate(h *hmap, oldbuck *bucket, newbuck *[]keyvalue) {
// ...
}

map扩容

双倍扩容

go语言中的哈希表在map的数组容量达到一定程度时,就会自动进行扩容。扩容的依据是当前已存储的元素数量和数组的长度之间的比值:

  • 当map的已存储元素数量小于map数组长度的一半时,元素的数量未达到哈希表效率的最大值,无需扩容;

  • 当map已存储的元素数量大于等于map数组长度的一半时,哈希表的查找效率已达到最大值,所以需要扩容。

go语言的map会优先选择数组大小为原数组大小的2倍,以确保map在存储过程中有足够的空间存放新的元素。当元素数量达到85%时,go语言就会再次对数组进行扩容,此时数组长度翻倍,以保证数组长度和元素数量的比例始终维持在0.75左右,以平衡效率和空间占用。

growing过程

当映射中的元素数量超过85%时,go语言就会触发map的扩容过程。在扩容的过程中,map会将原有的元素复制到新的数组中,并将新数组的初始大小设置为原数组的2倍。对于发生哈希冲突的元素,需要在新的数组中重新计算哈希地址。

避免溢出

当数组中元素的数量超过0x7fffffff(2^31-1,即int类型的最大值)时,就会发生溢出,此时数组的大小将无法达到原数组的2倍。所以go语言会在初始创建map时,为其初始化一个较小的数组,并设置map的b值,以便在元素数量超过限制时再次进行扩容。当map中元素的数量超过阈值时,会再次翻倍,直到数组大小小于0x7fffffff为止。

代码分析

hashmap.go包含在go语言源码中的src/container/map.go文件中。其中map结构体的定义和growing实现都在runtime包中,在src/runtime/map.go文件中。

附录

  • 为什么哈希表的容量要设置为2的n次幂?为什么不是其他数字?

  • go语言中的map是如何进行线程安全的?原理是什么?

  • map的数据结构是怎样的?如何实现键值对的查找、添加、删除操作?

  • 如何实现growing过程?

  • 为什么map的扩容条件是85%,而不是100%?

  • 在go语言中如何创建map?

  • 为什么哈希冲突处理策略有开放寻址法、再哈希法和链地址法?

  • 如果存在冲突,键值对是如何存储在数组中的?

  • 为什么growing过程中会创建一个较大的临时数组,而不是直接在原数组上扩展空间?

  • 如何实现map的迭代?

总结

本节我们学习了go语言中的map数据类型,使用方法以及map的数据安全问题。

到此这篇关于golang map简介以及底层原理的文章就介绍到这了,更多相关golang map详解内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

  • Go语言中的通道chan使用指南

    Go语言中的通道chan使用指南

    go 语言的通道(chan)是实现并发编程的核心工具之一,它为 goroutine 之间的通信提供了一种简单而高效的方式。在这篇文章中,我们将深入探讨通道的使用... [阅读全文]
  • Go语言实现支付宝支付与退款详解

    Go语言实现支付宝支付与退款详解

    在当今数字支付的时代,移动支付已经成为各行各业的主流选择。支付宝作为中国最大的支付平台之一,提供了丰富的支付和退款api,供开发者集成到各种应用中。上期我已经介... [阅读全文]
  • GO语言中ni,零值与空结构体的使用

    GO语言中ni,零值与空结构体的使用

    go语言的初学者,特别是java开发者新学习go语言,对于一些和java类似但是又有差异的概念很容易混淆,比如说go中的零值,nil 和 空结构体。本文就来详细... [阅读全文]
  • Go语言中的网络编程实现方式

    Go语言中的网络编程实现方式

    引言go语言作为一种简洁而强大的编程语言,在网络编程方面表现尤为出色。其内置的net包提供了丰富的网络i/o基础设施,支持tcp、udp协议,以及dns解析等功... [阅读全文]
  • Go语言Seeker接口与文件断点续传实战教程

    seeker接口在现代软件开发中,高效的输入输出(i/o)操作是提高程序性能的关键之一。特别是在处理大量数据时,i/o操作的效率直接影响到应用程序的响应速度和用户体验。go语言标准…

    2024年11月03日 前端脚本
  • Go语言内建函数len的使用

    Go语言内建函数len的使用

    在 go 语言中,len 是一个非常常用的内建函数,它用于获取各种数据类型的长度或大小。掌握 len 的使用方法,可以帮助我们更高效地处理数据结构。本文将详细介... [阅读全文]

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com