【Go语言精进之路】构建高效Go程序:了解map实现原理并高效使用

在这里插入图片描述

🔥 个人主页:空白诗
🔥 热门专栏:【Go语言精进之路】

在这里插入图片描述

文章目录

    • 引言
    • 一、什么是`map`
      • 1.1 `map`的基本概念与特性
      • 1.2 map的初始化与零值问题
      • 1.3 `map`作为引用类型的行为
    • 二、`map`的基本操作
      • 2.1 插入数据
      • 2.2 获取数据个数
      • 2.3 查找和数据读取
      • 2.4 删除数据
      • 2.5 遍历数据
    • 三、map的内部实现
      • 3.1 初始状态
      • 3.2 `map`扩容
      • 3.3 `map`并发
    • 四、尽量使用cap参数创建map
    • 五、总结

在这里插入图片描述

引言

在Go语言中,map 是一种无序的键值对集合,它以其高效的查找、插入和删除操作而闻名。了解 map 的基本概念、特性和内部实现机制,对于编写高效且稳定的Go代码至关重要。本文将深入探讨 map 的各个方面,包括其初始化、基本操作、内部实现细节,并讨论为何在创建 map 时应尽量使用带有容量提示参数的做法。


一、什么是map

在这里插入图片描述

1.1 map的基本概念与特性

map是Go语言中的一种内建引用类型,它表示一组无序的键值对集合。每个键值对用冒号“:”分隔,其中键(key)是唯一的,用于标识对应的值(value)。map允许我们根据特定的键快速检索、更新或删除对应的值。

在Go语言中,map对值(value)的数据类型没有特定限制,它可以是任意类型,包括基本类型、结构体、自定义类型等。但是,键(key)的类型有严格要求:key的类型必须可以通过“==”和“!=”操作符进行比较,这意味着键的类型需要是可比较的。因此,像函数、map和切片这样不可比较的类型不能作为map的键

1.2 map的初始化与零值问题

需要注意的是,map类型不支持“零值可用”,也就是说,未显式初始化的map变量其默认值为nil。尝试对nilmap变量进行操作将会导致运行时错误(panic)。例如:

var m map[string]int  // 此时m的值为nil
// 下面的操作将会导致运行时panic,因为m未被初始化
m["key"] = 1 // panic: assignment to entry in nil map

为了避免这种情况,我们需要在使用map之前对其进行初始化。可以通过以下两种方式之一来初始化map

  1. 使用make函数初始化:
m := make(map[string]int)
m["key"] = 1 // 现在这是安全的,因为m已经被初始化
  1. 使用字面量初始化:
m := map[string]int{"key": 1}
// 或者
m := map[string]int{}
m["key"] = 1 // 同样是安全的,因为m已经被初始化

初始化后的map可以被安全地用于存储和检索键值对,而不会导致运行时错误。在Go程序中,map是非常有用的数据结构,特别适用于需要根据键快速查找、添加或删除相应值的场景。

1.3 map作为引用类型的行为

和切片一样,map也是引用类型。这意味着,当你将一个map类型的变量传递给函数时,实际上传递的是指向底层数据结构的指针,而不是整个数据结构的拷贝。因此,将map类型变量作为函数参数传入不会有很大的性能消耗。

此外,由于在函数内部和外部引用的是同一个底层数据结构,所以在函数内部对map变量的修改(如添加、删除键值对或更新值)在函数外部也是可见的。这种特性使得map在需要在多个函数或方法间共享和修改数据时非常有用。

以下是一个示例,展示了在函数内部修改map,并在函数外部观察到这些修改:

package mainimport "fmt"func modifyMap(m map[string]int) {// 在函数内部修改mapm["apple"] = 5m["banana"] = 10
}func main() {// 初始化一个mapfruitMap := make(map[string]int)// 调用函数,传入map作为参数modifyMap(fruitMap)// 打印修改后的map,可以看到在modifyMap函数中所做的修改fmt.Println(fruitMap) // 输出: map[apple:5 banana:10]
}

在这个例子中,modifyMap函数接收一个map作为参数,并在函数内部添加了两个键值对。当函数执行完毕后,main函数中的fruitMap已经被修改,反映了modifyMap函数中所做的更改。这是因为map是引用类型,modifyMap接收的是fruitMap的引用,因此对它的任何修改都会反映在原始map上。


二、map的基本操作

在这里插入图片描述

2.1 插入数据

当面对一个非nilmap类型变量时,我们可以向其中插入符合map类型定义的任意键值对。值得注意的是,如果试图插入的键(key)已经存在于map中,那么新的值将会覆盖旧的值。Go运行时会管理map内部的内存,因此,除非系统内存耗尽,否则我们不必担心向map中插入大量数据。

m := make(map[string]int)
m["apple"] = 5  // 插入键值对 "apple": 5
m["apple"] = 7  // 更新键 "apple" 的值为 7,旧值5被覆盖
m["banana"] = 10 // 插入键值对 "banana": 10

在上述代码中,我们首先创建了一个从string类型到int类型的map。然后,我们插入了键值对"apple": 5。紧接着,我们尝试再次插入键"apple",但这次赋予它一个新的值7。由于这个键已经存在于map中,因此旧的值5会被新的值7覆盖。最后,我们插入了一个新的键值对"banana": 10

这种覆盖行为是map的一个重要特性,它允许我们根据需要更新存储在map中的值。在实际编程中,这一特性非常有用,比如当我们需要根据某些条件动态改变值时。

2.2 获取数据个数

要获取map中数据的个数,可以使用内置的len()函数。

count := len(m)
fmt.Println("Number of items in map:", count) // 输出map中的元素个数

len(m)返回m中当前存储的键值对数量。

2.3 查找和数据读取

可以根据键来查找和读取map中的数据。如果键不存在,则返回该类型的零值。

value, exists := m["apple"] // 查找键为"apple"的值,并检查键是否存在
if exists {fmt.Println("The value of 'apple' is:", value)
} else {fmt.Println("'apple' does not exist in the map.")
}

使用value, exists := m[key]的格式可以同时获取键对应的值和该键是否存在。如果键存在,existstrue,并且value为该键对应的值;如果键不存在,existsfalsevalue为该类型的零值。

2.4 删除数据

要从map中删除一个键值对,可以使用delete()函数。

delete(m, "banana") // 删除键为"banana"的键值对

delete(m, key)函数会从m中删除与key关联的键值对。如果key不存在,则delete什么也不做。

2.5 遍历数据

可以使用range关键字来遍历map中的所有键值对。

package mainimport "fmt"func main() {m := map[int]int{1: 11, 2: 12, 3: 13,}fmt.Printf("{ ")for key, value := range m {fmt.Printf("key: %d, value: %d  ", key, value)}fmt.Printf(" }\n")
}

range m会迭代m中的所有键值对,每次迭代都会返回当前的键和值。在上面的循环中,keyvalue分别被赋值为当前迭代的键和值,然后打印出来。
在这里插入图片描述

上面的输出结果非常理想,给我们的表象是迭代器按照map中的元素插入次序逐一遍历。那让我们再多遍历几次这个map

package mainimport "fmt"func doIteration(m map[int]int) {fmt.Printf("{ ")for key, value := range m {fmt.Printf("key: %d, value: %d  ", key, value)}fmt.Printf(" }\n")
}func main() {m := map[int]int{1: 11,2: 12,3: 13,}for i := 0; i < 3; i++ {doIteration(m)}
}

在这里插入图片描述

我们看见对同一map进行多次遍历,遍历的元素次序并不相同。这是因为Go运行时在初始化map迭代器时对起始位置做了随机处理。因此千万不要依赖遍历map所得到的元素次序


三、map的内部实现

在这里插入图片描述
和切片相比,map类型的内部实现要复杂得多。Go运行时使用一张哈希表来实现抽象的map类型,运行时实现了map操作的所有功能,包括查找、插入、删除、遍历等。本文这里只做一些简单的介绍。

3.1 初始状态

在Go语言中,当一个map被初始化时,它会分配一个较小的内存空间来存储键值对数据。这个初始的内存空间包含一定数量的桶(buckets),每个桶能够存储一个或多个键值对。初始状态下,这些桶都是空的。

map的初始化可以通过字面量、make函数或者直接使用map类型进行。例如:

// 使用字面量初始化
m1 := map[string]int{"apple": 5, "banana": 10}// 使用make函数初始化
m2 := make(map[string]int)// 直接声明map类型变量(需要后续进行初始化)
var m3 map[string]int
m3 = make(map[string]int)

在初始化时,map会预留一定的空间以准备存储键值对,但这个初始空间相对较小。

3.2 map扩容

map中的元素数量增加,负载因子(已存储的键值对数量与桶的数量的比例)也会随之增加。当负载因子超过某个预定的阈值时,map会进行扩容以保证性能。

扩容过程中,map会创建一个更大的桶数组,并且重新计算所有现有键值对的哈希值,将它们重新分布到新的桶数组中。这个重新哈希和分布的过程是为了确保键值对能够更均匀地分散在新的桶中,从而减少哈希冲突并提高查找效率。

扩容是一个相对昂贵的操作,因为它涉及到内存分配和大量数据的迁移。因此,在实际使用中,如果可能的话,最好提前预估map的大小并一次性分配足够的空间。

3.3 map并发

Go语言的map类型并不是并发安全的。这意味着如果多个goroutine同时对一个map进行读写操作,就可能导致数据竞争(data race)和不可预知的行为。

为了在并发环境中安全地使用map,有几种常见的解决方案:

  1. 使用互斥锁(Mutex):通过使用sync.Mutexsync.RWMutex来同步对map的访问。在每次读写map之前,先获取锁,操作完成后再释放锁。

  2. 使用sync.Map:Go语言标准库提供了一个并发安全的map实现,即sync.Map。它内部使用了分段锁和其他优化技术来提供高效的并发访问。

  3. 通道(Channel):另一种方法是使用Go的通道来序列化对map的访问。通过将所有对map的操作都通过一个或多个通道来进行,可以确保在同一时间只有一个goroutine能够访问map

在实际应用中,选择哪种并发控制方法取决于具体的使用场景和性能要求。对于简单的用例,使用互斥锁可能就足够了;而在需要高并发性能的场景中,sync.Map可能更为合适。


四、尽量使用cap参数创建map

在这里插入图片描述
由于扩容是一个相对昂贵的操作,因为它涉及到内存分配和大量数据的迁移,因此,如果可以的话我们最好对map使用规模做出粗略的估算,并使用cap参数对map实例进行初始化。

当你创建一个 map 而不指定容量时,Go 会自动为你分配一个初始的、未指定的容量。这个容量足以满足初始需求,并且随着 map 中元素的增加,Go 的运行时会自动管理其内部结构的大小调整,以容纳更多的元素。这是最常见也是最简单的初始化方式。

m := make(map[string]int)

如果你在创建 map 时明确指定了 cap 参数,你是在给 Go 提供一个关于你期望 map 最终可能包含多少个键值对的提示。这有助于减少 map 在增长过程中需要重新分配内存的次数,从而提高效率,尤其是在你知道 map 大致会有多大时。但请注意,指定的 cap 是一个提示而不是严格的限制,map 的实际容量可能会略高于指定的值,且 map 仍然可以在达到这个预设容量后继续增长。

m := make(map[string]int, 100)

优缺点分析:

  • 不使用 cap:简化初始化过程,让Go自动管理容量,适用于大多数情况,特别是当你不确定map最终大小时。
  • 使用 cap:通过预先估计map的大小,可以略微优化性能,减少动态扩容的次数,适合于明确知道或能估算map容量的场景。

选择是否使用 cap 主要取决于你对map最终规模的了解程度和对性能的特定需求。在不需要精确控制初始容量的情况下,省略 cap 是一个简洁且有效的方法。然而,如果你正处理大量数据且关心性能优化,明智地设定初始容量可以带来益处。

下面对两种初始化方式的性能进行对比:

package mainimport "testing"const mapSize = 10000func BenchmarkMapInitWithoutCap(b *testing.B) {for i := 0; i < b.N; i++ {m := make(map[int]int)for i := 0; i < mapSize; i++ {m[i] = i}}
}
func BenchmarkMapInitWithCap(b *testing.B) {for i := 0; i < b.N; i++ {m := make(map[int]int, mapSize)for i := 0; i < mapSize; i++ {m[i] = i}}
}

BenchmarkMapInitWithoutCap函数执行以下操作:

  1. 它使用一个循环,该循环将运行b.N次,其中b.Ntesting.B提供的,表示基准测试应该运行的次数。这是为了确保我们获得足够的数据点来平均性能测试结果,从而获得更准确的数据。

  2. 在每次循环中,它创建一个新的map,没有指定初始容量(make(map[int]int))。

  3. 然后,它向这个map中插入mapSize(即10000)个键值对,其中键和值都是循环变量i

这个基准测试的目的是测量在不指定初始容量的情况下,初始化并填充一个map的性能。

执行结果如下:
在这里插入图片描述

BenchmarkMapInitWithCap函数与BenchmarkMapInitWithoutCap非常相似,但有一个关键区别:

  1. 在创建map时,它使用make(map[int]int, mapSize)来指定一个初始容量提示,这个容量提示等于将要插入的键值对的数量(即10000)。

这个基准测试的目的是测量在指定了与将要插入的键值对数量相等的初始容量提示的情况下,初始化并填充一个map的性能。

下面是执行结果:
在这里插入图片描述
可以看出,使用cap参数的map实例的平均写性能是不使用cap参数的2倍。


五、总结

本文通过详细阐述了Go语言中 map 的基本概念、特性及其作为引用类型的行为,介绍了 map 的基本操作如插入、获取数据个数、查找、删除和遍历数据等。同时,深入剖析了 map 的内部实现,包括其初始状态、扩容机制以及并发问题。最后,本文强调了在使用 map 时,为了提高性能和减少内存重新分配的次数,应尽量在创建时提供合理的容量提示参数。通过全面理解 map 的工作原理和最佳实践,开发者可以更加有效地利用这一强大的数据结构来优化程序性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/26108.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前端开发部署:Visual Studio Code + vue

〇 说明 本教程全部采用默认安装路径&#xff0c;因为在进行自定义路径安装的时候&#xff0c;需要配置各种环境变量&#xff0c;在这个配置过程中&#xff0c;可能出现各种很混乱的问题。 一 安装Node.js 1 下载https://nodejs.org/en 2 按照默认NEXT执行 C:\Program Files…

文件传输系统主要用于哪些场景?要如何选型?

文件传输系统是一种用于在不同设备、网络或地理位置之间传输文件的产品解决方案&#xff0c;在各行各业中的应用还是很广泛的。 文件传输系统可以应用于多种场景&#xff0c;主要包括&#xff1a; 1、企业内部文件共享&#xff1a;在公司内部不同部门或团队之间共享文件&#…

9、编写业务逻辑

9、编写业务逻辑 9.1 编写博客接口(新增和查询一起编写了) 响应实体:(随便封装的,可以根据自己的想法封装) // entity/Response package com.example.fullstackblogback.commen;import lombok.Data;import java.util.List;@Data public class Response<T> {pri…

[经验] 梦见自己游泳是什么意思 周公解梦 #职场发展#微信#媒体

梦见自己游泳是什么意思 周公解梦 1、梦见自己游泳 梦见自己游泳是一种非常常见的梦境&#xff0c;而这个梦境通常代表着我们内心深处的渴望和憧憬。 游泳是一项需要技巧和勇气的运动&#xff0c;它需要我们在水中保持平衡和控制自己的呼吸。因此&#xff0c;梦见自己游泳通常…

代码随想录算法训练营第三十五天| 1005.K次取反后最大化的数组和、134. 加油站、135. 分发糖果

LeetCode 1005.K次取反后最大化的数组和 题目链接&#xff1a;https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/description/ 文章链接&#xff1a;https://programmercarl.com/1005.K%E6%AC%A1%E5%8F%96%E5%8F%8D%E5%90%8E%E6%9C%80%E5%A4%A7%E5%8C%9…

idea开发工具清除Git凭证(含Git凭证管理策略)

前言 网上很多人出现这个问题&#xff0c;也有很多文章或博客来说明这个问题&#xff0c;但是几乎都没有说到点子上&#xff0c;全网几乎都说清除credential.helper配置或者清空windows凭证管理器&#xff0c;还有一些文章说清除IDEA缓存&#xff0c;其实都是不对的。 creden…

黑龙江三级等保测评内容与等级划分

一、黑龙江等保三级测评内容 黑龙江等保三个层次&#xff0c;也就是三个级别的信息安全防护&#xff0c;这是我们国家的一项基础性的信息安全体系。在此基础上&#xff0c;提出了一种适用于非银行机构的最高级别的保障制度&#xff0c;即当该制度遭到破坏时&#xff0c;可能给…

Bankless:为什么 AI 需要 Crypto 的技术?

原文标题&#xff1a;《Why AI Needs Crypto’s Values》 撰文&#xff1a;Arjun Chand&#xff0c;Bankless 编译&#xff1a;Chris&#xff0c;Techub News 原文来自香港Web3媒体&#xff1a;Techub News 人工智能革命的梦想一直是一把双刃剑。 释放人工智能的潜力可以解…

springboot3一些听课笔记

文章目录 一、错误处理机制1.1 默认1.2 自定义 二、嵌入式容器 一、错误处理机制 1.1 默认 错误处理的自动配置都在ErrorMvcAutoConfiguration中&#xff0c;两大核心机制&#xff1a; ● 1. SpringBoot 会自适应处理错误&#xff0c;响应页面或JSON数据 ● 2. SpringMVC的错…

深入解析ETL与ELT架构:数据集成技术的演进与发展

摘要&#xff1a;随着大数据时代的到来&#xff0c;数据集成成为企业信息化建设的重要环节。本文将深入探讨ETL与ELT两种架构&#xff0c;分析它们在数据处理、性能、可扩展性等方面的差异&#xff0c;为企业数据集成提供技术指导。 一、引言 在大数据时代&#xff0c;企业需要…

13- 函数的定义与使用+形参实参区分

13- 函数的定义与使用形参实参区分 文章目录 13- 函数的定义与使用形参实参区分一、函数的定义与使用1.1 函数的结构1. 函数头2. 函数体 1.2 示例代码例子 1&#xff1a;无参数和无返回值的函数例子 2&#xff1a;带参数和返回值的函数 1.3 函数的基本语法1.4 函数的使用示例例…

Faster-RCNN基本思想和网络结构

简单来说&#xff0c;Faster RCNN RPN Fast RCNN RPN 是指 Region Proposal Network&#xff0c;建议区域生成网络。 Faster RCNN 中用 RPN 来代替了 Fast RCNN 中的SS算法。 算法流程&#xff1a; &#xff08;1&#xff09;将图像输入CNN网络得到相应的特征图。 &#x…

单机多卡分布式训练策略——MirroredStrategy

前言 分布式训练是一种用于在多个设备或机器上同时训练深度学习模型的技术&#xff0c;它有助于减少训练时间&#xff0c;允许使用更多数据更快训练大模型。分布式训练重点关注数据并行性&#xff0c;本次试验使用的是单机多卡的分布式训练策略&#xff0c;也就是 MirroredStr…

算法题目学习汇总

1、二叉树前中后序遍历:https://blog.csdn.net/cm15835106905/article/details/124699173 2、输入一棵二叉搜索树&#xff0c;将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点&#xff0c;只能调整树中结点指针的指向。 public class Solution {private Tr…

多模态AI的挑战与早期壁垒的构建

伴随着Sora、GPT40的推出&#xff0c;多模态AI逐渐成为研究的热点和应用的趋势。然而&#xff0c;多模态AI的发展并非一帆风顺&#xff0c;它面临着诸多挑战和壁垒。 一、多模态AI的难点 多模态AI的核心在于将不同模态的信息&#xff08;如文本、图像、音频、视频等&#xff…

离线翻译器下载哪个好?这几个翻译器用过的人都说好

面对跨文化交流的挑战&#xff0c;如国际旅行或多元工作环境&#xff0c;语言障碍尤为突出。 特别是在信号弱或无网络覆盖的地区&#xff0c;翻译需求变得更加迫切。此时&#xff0c;一款优质的离线翻译app显得尤为重要。它能够在没有网络支持的情况下提供即时翻译服务&#x…

CF297C Splitting the Uniqueness 题解

CF297C Splitting the Uniqueness 题解 非常好构造题&#xff0c;使我的草稿纸旋转。 解法 我们记输入的数组为 a a a&#xff0c;需要输出的两个数组为 b , c b,c b,c&#xff08;因为当时起变量名起的&#xff09;。 考虑利用 a i a_i ai​ 互不相同的性质。 先将 a…

二叉树顺序结构——堆的结构与实现

二叉树顺序结构——堆的结构与实现 一、二叉树的顺序结构二、堆的概念及结构三、堆的实现堆向下调整算法堆的创建建堆时间复杂度堆的插入(堆向上调整算法)堆的删除堆的代码实现(使用VS2022的C语言)初始化、销毁构建、插入、删除返回堆顶元素、判空、返回有效元素个数 四、完整 …

20240610 基于QGIS生成地区示意图的地图shp文件

目录 本文目标前置条件具体步骤1. 创建Project2. 插入世界地图3. 对地区示意图进行地理匹配4. 创建shp文件&#xff0c;勾画轨迹 注意事项 本文目标 基于QGIS生成地区示意图的地图shp文件&#xff0c;此shp文件可以用来学习&#xff0c;但是未经审批不可用于发表。 前置条件 …

Python基础教程(十一):数据结构汇总梳理

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; &#x1f49d;&#x1f49…