Golang map_golang make map_李培冠的博客-程序员宝宝

技术标签: Golang  golang  编程语言  go  后端  

前言

哈希表是一种巧妙并且实用的数据结构。它是一个无序的 key/value对 的集合,其中所有的 key 都是不同的,然后通过给定的 key 可以在常数时间复杂度内检索、更新或删除对应的 value。

在 Go 语言中,一个 map 就是一个哈希表的引用,map 类型可以写为 map[K]V,其中 K 和 V 分别对应 key 和 value。map 中所有的 key 都有相同的类型,所有的 value 也有着相同的类型,但是 key 和 value 之间可以是不同的数据类型。其中 K 对应的 key 必须是支持 == 比较运算符的数据类型(切片、函数等不支持),所以 map 可以通过测试 key 是否相等来判断是否已经存在。虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做 key 类型则是一个坏的想法。对于 V 对应的 value 数据类型则没有任何的限制。

  • map 是无序的
  • 在 Go 语言中的 map 是引用类型,必须初始化才能使用

Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。由于 map 是无序的,我们无法决定它的返回顺序。

map 的定义

可以使用内建函数 make 也可以使用 map 关键字来定义 map:

// 使用 make 函数
m := make(map[keyType]valueType)
// 长度为 0 的 map
m := make(map[keyType]valueType, 0)

// 声明变量,默认 map 是 nil
var m map[keyType]valueType
// 长度为 0 的 map
var m map[keyType]valueType{
    }

其中:

  • m 为 map 的变量名。
  • keyType 为键类型。
  • valueType 是键对应的值类型。

在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的。但是如果我们提前知道 map 需要的长度,最好指定一下。

我们可以用 len(m) 来查看 map 的长度。注意,使用 cap(m) 会报错(cap 支持 数组、指向数组的指针、切片、channel):

invalid argument m (type map[string]int) for cap

如果不初始化 map,那么就会创建一个 nil map。nil map 不能用来存放键值对。如果向一个 nil 值的 map 存入元素将导致一个 panic 异常:

下面我们用 make 函数创建一个 map:

ages := make(map[string]int)

当然,我们也可以直接创建一个 map 并且指定一些最初的值:

ages := map[string]int{
    
    "Conan": 18,
    "Kidd": 23,
}

这种就相当于:

ages := make(map[string]int)
ages["Conan"] = 18
ages["Kidd"] = 23

所以,另一种创建空(不是 nil)的 map 方法是:

ages := map[string]int{
    }

map 在定义时,key 是唯一的,不允许重复(value 可以重复)。下面的程序会报错

ages := map[string]int{
    
    "Conan": 18,
    "Conan": 23,
}

但是之后在对 map 赋值时,则会覆盖原来的 value

ages["Conan"] = 18
ages["Conan"] = 23
fmt.Println(ages["Conan"])  // 23

map 类型的零值是 nil,也就是没有引用任何哈希表,其长度也为 0.

var ages map[string]int
fmt.Println(ages == nil)  // true
fmt.Println(len(ages))  // 0

map 的基本使用

增加 map 的值很简单,只需要 m[key] = value 即可,比如:

ages := make(map[string]int)
ages["Conan"] = 18
ages["Kidd"] = 23

使用内置的 delete 函数可以删除元素,参数为 map 和其对应的 key,没有返回值:

delete(ages, "Conan")

注意:即使这些 key 不在 map 中也不会报错。

修改 map 的内容和 增 的写法类似,只不过 key 是已存在的,如果不存在,则为增加,例如:

ages := map[string]int{
    
    "Conan": 18,
    "Kidd": 23,
}
ages["Conan"] = 21

map 中的元素通过 key 对应的下标语法访问:

ages["Conan"] = 18
fmt.Println(ages["Conan"])  // 18

要想遍历 map 中全部的键值对的话,可以使用 range 风格的 for 循环实现,和之前的 slice 遍历语法类似。例如:

for key, value := range ages {
    
    fmt.Println(key, value)
}

如果用不到 value,无需使用匿名变量 _,直接不写即可:

for key := range ages {
    
    fmt.Println(key)
}

如果查找失败也没有关系,程序也不会报错,而是返回 value 类型对应的零值。例如:

ages := map[string]int{
    
    "Conan": 18,
    "Kidd": 23,
}
fmt.Println(ages["Lan"])  // 0

通过 key 作为索引下标来访问 map 将产生一个 value。如果 key 在 map 中是存在的,那么将得到与 key 对应的 value;如果 key 不存在,那么将得到 value 对应类型的零值。

但是有时候我们需要知道对应的元素是否真的是在 map 之中。比如,如果元素类型是一个数字,你需要区分一个已经存在的 0,和不存在而返回零值的 0。例如:

ages := map[string]int{
    
    "Conan": 18,
    "Kidd": 23,
}
// 如果 key 存在,则 ok = true;不存在,ok = false
if value, ok := ages["Conan"]; ok {
    
    fmt.Println(value)
} else {
    
    fmt.Println("key 不存在")
}

在这种场景下,map 的下标语法将产生两个值;第二个是一个布尔值,用于报告元素是否真的存在。布尔变量一般命名为 ok,特别适合马上用于 if 条件判断部分。

map 的迭代顺序是不确定的。有没有什么办法可以顺序的打印出 map 呢?我们可以借助切片来完成。先将 key(或者 value)添加到一个切片中,再对切片排序,然后使用 for-range 方法打印出所有的 key 和 value。如下所示:

package main

import (
	"fmt"
	"sort"
)

func main() {
    
	// 创建一个 ages map,并给三个值
	ages := make(map[string]int)
	ages["Conan"] = 18
	ages["Kidd"] = 23
	ages["Lan"] = 19

	// 创建一个切片用于给 key 进行排序
	var names []string
	for name := range ages {
    
		names = append(names, name)
	}
	sort.Strings(names)

	// 循环打印出 map 中的值
	for _, name := range names {
    
		fmt.Printf("%s\t%d\n", name, ages[name])
	}
}

因为我们一开始就知道 names 的最终大小,因此给切片分配一个合适的容量大小将会更有效。下面的代码创建了一个空的切片,但是切片的容量刚好可以放下 map 中全部的 key:

names := make([]string, 0, len(ages))

当然,如果使用结构体切片,这样就会更有效:

type name struct {
    
    key string
    value int
}

map 之间不能进行相等比较;唯一的例外是和 nil 进行比较。要判断两个 map 是否包含相同的 key 和 value,我们必须通过一个循环实现:

func equalMap(x, y map[string]int) bool {
    
    // 长度不一样,肯定不相等
    if len(x) != len(y) {
    
        return false
    }
    for k, xv := range x {
    
        if yv, ok := y[k]; !ok || xv != yv {
    
            return false
        }
    }
    return true
}

map 作为函数参数

map 作为函数参数是地址传递(引用传递),作返回值时也一样。

在函数内部对 map 进行操作,会影响主调函数中实参的值。例如:

func foo(m map[string]int) {
    
    m["Conan"] = 22
    m["Lan"] = 21
}

func main() {
    
    m := make(map[string]int, 2)
    m["Conan"] = 18
	fmt.Println(m)  // map[Conan:18]
	foo(m)
	fmt.Println(m)  // map[Conan:22 Lan:21]
}

并发环境中使用的 map:sync.Map

Go 语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的

下面我们来看一下在并发情况下读写 map 时会出现的问题,代码如下:

// 创建一个 map
m := make(map[int]int)

// 开启一个 go 程
go func () {
    
    // 不停地对 map 进行写入
    for true {
    
        m[1] = 1
    }
}()

// 开启一个 go 程
go func() {
    
    // 不停的对 map 进行读取
    for true {
    
        _ = m[1]
    }
}()

// 运行 10 秒停止
time.Sleep(time.Second * 10)

运行代码会报错,错误如下:

fatal error: concurrent map read and map write

当两个并发函数不断地对 map 进行读和写时,map 内部会对这种并发操作进行检查并提前发现。

当我们需要并发读写时,一般的做法是加锁,但是这样性能不高。

Go 语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map。

sync.Map 有以下特性:

  • 无须初始化,直接声明即可
  • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用:Store 表示存储,Load 表示获取,Delete 表示删除。
  • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时返回 true,终止迭代遍历时,返回 false。

并发安全的 sync.Map 示例代码如下:

package main

import (
	"fmt"
	"sync"
)

func main() {
    
	var ages sync.Map

	// 将键值对保存到 sync.Map
	ages.Store("Conan", 18)
	ages.Store("Kidd", 23)
	ages.Store("Lan", 18)

	// 从 sync.Map 中根据键取值
	age, ok := ages.Load("Conan")
	fmt.Println(age, ok)

	// 根据键删除对应的键值对
	ages.Delete("Kidd")
	fmt.Println("删除后的 sync.Map: ", ages)

	// 遍历所有 sync.Map 中的键值对
	ages.Range(func(key, value interface{
    }) bool {
    
		fmt.Println(key, value)
		return true
	})
}

sync.Map 没有提供获取 map 数量的方法,替代方法是在获取 sync.Map 时遍历自行计算数量,sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。

所以,我们用 sync.Map 时进行同时读写是没问题的,示例代码如下:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
    
	var m sync.Map

	// 开启一个 go 程
	go func() {
    
		// 不停地对 map 进行写入
		for true {
    
			m.Store(1, 1)
		}
	}()

	// 开启一个 go 程
	go func() {
    
		// 不停的对 map 进行读取并打印读取结果
		for true {
    
			value, _ := m.Load(1)
			fmt.Println(value)
		}
	}()
	time.Sleep(time.Second * 10)
}

这时的结果就会一直输出 1。

练习

1、封装 wordCountFunc() 函数。接收一段英文字符串 str。返回一个 map,记录 str 中每个“单词”出现的次数。

示例:

输入:"I love my work and I love my family too"
输出:
    family:1
    too:1
    I:2
    love:2
    my:2
    work:1
    and:1

提示:使用 strings.Fields() 函数可提高效率

实现:

package main

import (
	"fmt"
	"strings"
)

func wordCountFunc(str string) map[string]int {
    
	// 使用 strings.Fields 进行拆分, 自动按照空格对字符串进行拆分成切片
	wordSlice := strings.Fields(str)
	// 创建一个用于存储 word 次数的 map
	m := make(map[string]int)

	// 遍历拆分后的字符串切片
	for _, value := range wordSlice {
    
		if _, ok := m[value]; !ok {
    
			// key 不存在
			m[value] = 1
		} else {
    
			// key 值已存在
			m[value]++
		}
	}
	return m
}

func main() {
    
	str := "I love my work and I love my family too"
	res := wordCountFunc(str)

	// 遍历 map, 展示每个 word 出现的次数
	for key, value := range res {
    
		fmt.Println(key, ": ", value)
	}
}

如需更深入的了解 map 的原理,推荐阅读这篇文章:深度解密Go语言之map

李培冠博客

欢迎访问我的个人网站:

李培冠博客:lpgit.com

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_33555334/article/details/107781880

智能推荐

C#中的委托delegate_30年后世界会是怎样的博客-程序员宝宝

C# 中的委托(Delegate)是什么?C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。C# 中的委托(Delegate)是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。委托:是一种定义方法签名的类型。当实例化委托时,您可以将其实例与任何具有兼容签名的方法相关联。您可以通过委托实例调用方法。C# 中的委托(Delegate)用于什么场...

Omap138开发板下以uboot2012.04.01为例分析uboot执行(一)_嵌入式攻城狮小白的博客-程序员宝宝

u-boot编译正确编译U-boot的代码的过程如下:首先是编译器的问题,确保GNU交叉编译工具链环境变量的设置生效。2)为特定的板子建立配置文件。输入:makeCROSS_COMPILE=arm-none-linux-gnueabi- da850sdi_tl_cofig .可以从boards.cfg中找到支持开发板的配置名称,配置好后,显示如下:...

GSEA富集分析_gsea nes负值的意义_修罗神天道的博客-程序员宝宝

GSEA定义Gene Set Enrichment Analysis (基因集富集分析)用来评估一个预先定义的基因集的基因在与表型相关度排序的基因表中的分布趋势,从而判断其对表型的贡献。其输入数据包含两部分,一是已知功能的基因集 (可以是GO注释、MsigDB的注释或其它符合格式的基因集定义),一是表达矩阵,软件会对基因根据其于表型的关联度(可以理解为表达值的变化)从大到小排序,然后判断基因集内每条注释下的基因是否富集于表型相关度排序后基因表的上部或下部,从而判断此基因集内基因的协同变化对表型变化的影响

sysvol 域控制器 文件_WinServer域控制器中重定位SYSVOL树_Daydayydayyy的博客-程序员宝宝

WinServer域控制器中重定位SYSVOL树作者 HonestQiao 2005年11月13日 16:00概要系统卷(或称 SYSVOL)是可由文件复制服务 (FRS) 复制的文件夹、文件系统重分析点和组策略设置的集合。复制功能可在域中的各域控制器之间分发组策略设置和脚本的一致副本。成员计算机和域控制器可通过两个共享文件夹 Sysvol 和 Netlogon 来访问 SYSVOL 树的内容。本...

suse Linux安装 rabbitmq创建用户失败怎么解决_"creating user \"heguang\" ... error: {undef, [{cr_java小黑仔的博客-程序员宝宝

suse Linux安装 rabbitmq创建用户失败怎么解决er admin adminCreating user “admin”Error: {undef,[{crypto,hash,[sha256,<<253,22,240,201,97,100,109,105,110>>],[]},{rabbit_password,hash,2,[{file,“src/r...

ACM算法专题_cyberpojice. cn_Tizzii的博客-程序员宝宝

专题一 简单搜索POJ 1321 棋盘问题POJ 2251 Dungeon MasterPOJ 3278 Catch That CowPOJ 3279 FliptilePOJ 1426 Find The MultiplePOJ 3126 Prime PathPOJ 3087 Shuffle'm UpPOJ 3414 PotsFZU 2150 Fire GameUVA 11624...

随便推点

系统分析与设计HW1_Messiahchen的博客-程序员宝宝

简答题软件工程的定义软件工程是对软件开发、操作和维护的系统化、规范化以及可量化方法的应用,是工程对软件的应用,用于研究发展软件的方法。解释导致 software crisis 本质原因、表现,述说克服软件危机的方法软件危机应用于早期计算机科学,它是指在规定时间内难以完成有用且高效的计算机程序。软件危机的是由计算机的能力快速增长且无法解决复杂问题造成的。随着软件越来越复杂,现有程序员缺乏有...

令人拍案叫绝的Wasserstein GAN 及代码(WGAN两篇论文的中文详细介绍)---------好理解!!_wasserstein [2] objective_Candy_GL的博客-程序员宝宝

作者:郑华滨链接:https://zhuanlan.zhihu.com/p/25071913来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 在GAN的相关研究如火如荼甚至可以说是泛滥的今天,一篇新鲜出炉的arXiv论文《Wassertein GAN》却在Reddit的Machine Learning频道火了,连Goodfellow都在帖子里和大家热烈讨论...

Linux环境下,使用Eclipse,创建C++工程 HelloWord_linux启动eclipse_Adunn的博客-程序员宝宝

# Linux环境下使用Eclipse,创建C++工程HelloWord本文是以Ubuntu20为开发环境,过程中重点处理了“没有规则可制作目标”的错误解决办法## 主要步骤**(1)打开Eclipse开发环境;****(2)新建工程****(3)处理错误:“没有规则可制作目标”**...

ConcurrentModificationException错误_iFADA的博客-程序员宝宝

一.问题:如果不加break会报ConcurrentModificationException异常1.第一种方法加break List<ShopcartBO> shopcartList = JsonUtils.jsonToList(shopcartJson, ShopcartBO.class); // 判断购物车中是否存在已有商品,如果有的话则删除 for (ShopcartBO sc: shopcartList) {

在计算机打开本地策略,windows操作系统在运行这个功能被禁止的情况下怎么打开计算机本地策略..._首夏的博客-程序员宝宝

Windows Server 2003操作系统问答集锦本文介绍了Windows Server 00操作系统使用过程中读者可能会常遇到的七种问题。  、如何打开 DirectX 的 DirectXD 硬件加速:   答:打开桌面属性,设置 -> 高级 -> 疑难问答 -> 硬件加速 -> 完全 -> 应用。运行 dxdiag.exe,打开显示选项卡,可看到 项全部启用了...

推荐文章

热门文章

相关标签