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

news2024/11/21 1:34:06

在这里插入图片描述

🔥 个人主页:空白诗
🔥 热门专栏:【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 main

import "fmt"

func modifyMap(m map[string]int) {
    // 在函数内部修改map
    m["apple"] = 5
    m["banana"] = 10
}

func main() {
    // 初始化一个map
    fruitMap := 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 main

import "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 main

import "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 main

import "testing"

const mapSize = 10000

func 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.coloradmin.cn/o/1811044.html

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

相关文章

Python 使用scrapy框架

1、安装scrapy 2、使用scrapy创建项目,在终端命令行 执行如下命令&#xff0c;会创建一个myproject项目 scrapy startproject myproject 3、创建完成后&#xff0c;目录结构如下 4、cd myproject进入项目 ,执行scrapy genspider weather ******&#xff0c;会在spiders下创建…

数据库期末设计——图书管理系统

目录 1.前置软件以及开发环境&#xff1a; 2.开发过程讲解 代码环节&#xff1a; 数据库代码 1.BookDao.java 2.BookTypeDao.java 3.UserDao.java 4.Book.java 5.BookType.java 6.User.java 7.DbUtil.java 8.Stringutil.java 9.BookAddInterFrm.java 10.BookMan…

知识图谱的应用---新零售

文章目录 新零售知识图谱构建过程典型应用 新零售 新零售&#xff0c;即个人、企业以互联网为依托&#xff0c;通过运用大数据、人工智能等先进技术手段并运用心理学知识&#xff0c;对商品的生产、流通与销售过程进行升级改造&#xff0c;进而重塑业态结构与生态圈&#xff0c…

LlamaIndex 四 数据连接器

前言 我们通过各项配置&#xff0c;理解了LlamaIndex在构建知识库和基于知识库的推荐两个阶段&#xff0c;怎么和业务相结合。本文&#xff0c;我们将开始深入理解LlamaIndex的各个模块。首先&#xff0c;LlamaIndex强大的Data Connector 数据连接器上场。 LlamaIndex擅长和各…

AI绘画Stable Diffusion必看:如何生成人物的全身照实战教程!

大家好&#xff0c;我是画画的小强 在小伙伴们私下留言中&#xff0c;关于如何实现人物的全身照一直有人咨询&#xff0c;看来这个问题还是困惑着不少小伙伴&#xff0c;今天我们就这个问题详细讨论一下。 一. 宽高比参数设置 在讨论如何生成图片的全身照之前&#xff0c;我…

关于用宽带(拨号)连接VPN无法上网,但是wifi或者热点就可以的问题

参考链接&#xff1a;https://zhuanlan.zhihu.com/p/580929250https://zhuanlan.zhihu.com/p/580929250 https://blog.csdn.net/Yaoyao2024/article/details/132245249文章浏览阅读10w次&#xff0c;点赞161次&#xff0c;收藏515次。很多同学在学习访问学校提供的资源时或者一…

等保一体机案例 | 黑龙江某事业单位档案管理系统借助捷云过二级等保

黑龙江某事业单位&#xff0c;聚焦产业发展的应用技术研究&#xff0c;兼顾重大技术应用的基础研究&#xff0c;满足广东省经济社会发展需要。 由于信创要求&#xff0c;也为了满足《关于加强国家电子政务工程建设项目信息安全风险评估工作的通知》&#xff08;发 改高技〔2008…

[图解]企业应用架构模式2024新译本讲解13-服务层1

1 00:00:00,150 --> 00:00:05,030 接下来我们来看服务层模式 2 00:00:06,070 --> 00:00:11,740 这也是领域逻辑类型的最后一个模式 3 00:00:13,700 --> 00:00:17,110 我们前面实际上已经见过了&#xff0c;事务脚本 4 00:00:17,240 --> 00:00:19,900 的时候&…

citys

城市边界电子围栏 初始化摄像头、灯光等 let renderer, scene, camera, stats, gui, texture;renderer new THREE.WebGLRenderer({logarithmicDepthBuffer: true,});renderer.setPixelRatio(window.devicePixelRatio);renderer.setSize(window.innerWidth, window.innerHeigh…

如何实现单例模式及不同实现方法分析-设计模式

这是 一道面试常考题&#xff1a;&#xff08;经常会在面试中让手写一下&#xff09; 什么是单例模式 【问什么是单例模式时&#xff0c;不要答非所问&#xff0c;给出单例模式有两种类型之类的回答&#xff0c;要围绕单例模式的定义去展开。】 单例模式是指在内存中只会创建…

一颗万能的PD协议芯片,能芯Type-C PD协议芯片“ECP5705”, 它是如何实现PD直流风扇应用呢?

文章目录 文章目录 前言 一、PD风扇方案介绍 二、芯片介绍 三、PD风扇-供电方式 四、能芯科技 PD 协议芯片ECP5705-应用场景 总结 前言 随着USB Type-C接口的普及和PD取电芯片的出现&#xff0c;使得小型家电和电动工具可以通过统一的USB Type-C接口进行充电&#xff0c;极大地…

台灯学生用哪个牌子最好?学生台灯十大名牌排行榜分享

近年来&#xff0c;我们注意到儿童近视的现象呈现出增多且趋于低龄化的趋势。这一变化&#xff0c;部分原因可以归咎于孩子们越来越多地使用电子产品&#xff0c;另一部分则与他们面临的学业压力增加有关。鉴于此&#xff0c;家长们在挑选儿童学习用品时变得格外谨慎&#xff0…

js实现简单计算器词法解析语法解析解释器,带可视化界面

代码 Lexer是词法解析器 Parser是语法解析器 Interpreter 是ast解释器 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0&q…

【启明智显芯片应用】Model3C芯片4.3寸拼图机应用方案

数据显示&#xff0c;618前期&#xff0c;早教启智、智能玩具、科学启蒙、数字阅读类产品销量增长迅猛。当下&#xff0c;90后新生代父母对于孩子的科学启蒙教育愈发重视&#xff0c;他们在给孩子选择学习产品时&#xff0c;越来越倾向于选择寓教于乐的益智类产品&#xff0c;而…

神奇!你见过生成Prompt的Prompt?

目录 先上干货 这个Prompt是怎么来的 道路是曲折的 总结 PART/ 01 先上干货 你怎么会想到这些场景呢&#xff0c;比如健身计划、英语学习、旅游规划等等&#xff0c;其实挺不好意思的&#xff0c;大家被我骗了&#xff0c;这些都是我使用一个Prompt解决的。 对&#xf…

使用OpenLLM在AMD GPU上的分步指南

Step-by-Step Guide to Use OpenLLM on AMD GPUs — ROCm Blogs 引言 OpenLLM是一个开源平台&#xff0c;旨在促进大型语言模型&#xff08;LLMs&#xff09;的部署和使用&#xff0c;支持多种模型&#xff0c;适应不同的应用&#xff0c;无论是在云环境还是本地环境中。在本教…

何为屎山代码?

在编程界&#xff0c;有一种代码被称为"屎山代码"。这并非指某种编程语言或方法&#xff0c;而是对那些庞大而复杂的项目的一种形象称呼。屎山代码&#xff0c;也被称为"祖传代码"&#xff0c;是历史遗留问题&#xff0c;是前人留给我们的"宝藏"…

丽水职业技术学院:以太彩光网络筑基教育信息化标杆之路

丽水职业技术学院作为教育信息化的先行者,是浙江省首批“浙江省高职高水平学校”、“浙江省教育信息化试点校单位”,也是“浙江省数字校园示范校”的一员。学院紧握“十四五”规划契机,全面加速数字化转型,旨在通过基础网络的革新、数字化教学的深化、信息服务的优化、学生管理…

wma和mp3哪个音质好?让我告诉你哪个更胜一筹

在数字音频领域&#xff0c;WMA和MP3是两种常见的音频格式&#xff0c;它们在网络上的音频传输和储存中都扮演着重要的角色。然而&#xff0c;许多人可能会对这两者之间的音质差异产生疑问&#xff0c;想知道哪一个更适合他们的需求。wma和mp3哪个音质好&#xff1f;在本文中&a…

Transformer介绍

Transformer的诞生 2018年Google发出一篇论文《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》, BERT模型横空出世, 并横扫NLP领域11项任务的最佳成绩&#xff01; 而在BERT中发挥重要作用的结构就是Transformer, 之后又相继出现XLNET&a…