java map字面量,go语言映射(map)要点总结

bVbCIU2

1. 定义

Go语言中映射是一种字典类型的数据结构,类似于c++和java中的hashmap,用于存储一系列无序的键值对。

映射是基于键来存储值。映射的优势是能够基于键快速索引数据。键就像索引一样,指向与该键关联的值,在内存中键值对的关系如下图所示。

bVbCHm1

和切片类似,映射维护的是底层数组的指针,属于引用类型。

2. 内部实现

映射是一个集合,可以使用类似处理数组和切片的方式迭代映射中的元素。但映射是无序的,不能对键值对进行排序。即使按顺序保存,每次迭代的时候顺序也不一样。无序的原因是映射的实现使用了散列表。

这个散列表是由hmap实现的,hmap中维护着存储键值对的bucket数组,hmap和bucket数组的关系如下图所示。

bVbCHuP

bucket是一个链表结构,每一个bucket后面连接的bucket表示bucket容量不够用时进行扩容添加新的bucket,bucket的数据结构如下图所示。

bVbCHyu

说到散列表,一定要有散列函数,散列函数是散列表中存取元素的关键。Go语言的映射中也有这样的散列函数(或叫哈希函数),但是Go语言把散列函数计算出的散列值可以说是用到了极致。它把散列值分成了高8位和低8位两部分,如下图所示。

bVbCHFt

散列值的作用

低8位散列值:用于寻找当前key位于哪个bucket;

高8位散列值:用于寻找当前key位于bucket中的哪个位置。

映射的存取过程:在存储,删除或者查找键值对的时候,都要先将指定的键传给散列函数得到一个散列值,然后根据散列值的低8位选中对应的桶,最终再根据桶中的索引(散列值的高8位)将键值对分布到这个桶里。随着映射中存储的增加,索引分布越均匀,访问键值对的速度就越快。因此,映射通过设置合理数量的桶平衡键值对的分布。整个过程如下图所示。

bVbCH9R

键值对的存储方式:键值对的存储不是以key1,value1,key2,value2这样的形式存储,主要是为了在key和value所占字节长度不同的时候,可以消除padding带来的空间浪费。

映射的扩容:当散列表的容量需要增长的时候,Go语言会将bucket数组的容量扩充一倍,产生新的bucket数组,并将旧数据迁移到新数组。

判断是否扩充的条件,就是散列表的加载因子,加载因子是一个阈值,表示散列表的空间利用率,Go语言map中的加载因子计算公式为:map长度/2^B,阈值是6.5,B表示已扩容的次数。

映射中数据的删除

如果key是指针类型的,直接将其置空,等待GC清除;

如果是值类型的,则清除相关内存;

对value做同样的操作;

把key对应的索引置为空。

3. 映射的创建

(1) 使用字面量创建

// 创建一个键为字符串类型,值为整形的map

m1 := map[string]int{"last":2019, "now":2020}

// 获取map中的元素

fmt.Println(m1["last"]) // 2019

fmt.Println(m1["now"]) // 2020

// 使用字面量创建一个空map

m2 := map[string]string{}

fmt.Println(m2) // map[]

映射的键的类型可以是内置类型,也可以是结构类型,但这个类型必须可以使用==运算符做比较。切片,函数以及包含切片的结构类型由于具有引用语义,不能作为映射的键,否则会造成编译错误。

(2) 使用内置的make函数来创建

m1 := make(map[int] string) // map的容量使用默认值

m1[1] = "lioney"

m2 := make(map[int] string, 10) // map的容量使用给定值

m2[1] = "carlos"

fmt.Println(m1[1]) // lioney

fmt.Println(m2[1]) // carlos

4.映射支持的操作

map中单个键值的访问格式为mapName[key],可以用于获取或更新map中的元素

可以使用for range遍历map中的元素,不保证每次迭代顺序一致

删除map中的某个键值对,使用语法delete(mapName, key)

使用内置函数len()获取map中保存的键值对数量,map中不支持cap()函数

package main

import "fmt"

func main() {

// 创建一个空的map

m1 := make(map[int] string)

// 向map中添加元素

m1[1] = "lioney"

m1[2] = "carlos"

m1[3] = "tom"

// 从map中删除键为3的元素

delete(m1, 3)

// len()表示map中键值对的数量

fmt.Println("len=", len(m1))

// 遍历map

for k, v := range m1 {

fmt.Println("key=", k, "value=", v)

}

}

上述代码编译后运行结果如下:

len= 2

key= 2 value= carlos

key= 1 value= lioney

Process finished with exit code 0

5. 映射的使用要点

(1) 对nil映射赋值会产生运行时错误

和切片类似,映射在使用时必须对其底层数组进行初始化,要么使用make进行初始化,要么使用字面量初始化,如果只是简单地声明了一个map,而没有进行初始化,就是nil映射,是不能对其赋值的,请看下面代码:

// 声明一个map

var colors map[string]string

// 将red加入colors

colors["red"] = "#da137" // panic: assignment to entry in nil map

可以做如下修改:

// 声明一个map

var colors map[string]string

// 对map进行初始化

colors = make(map[string]string)

// 将red加入colors

colors["red"] = "#da137" // no panic or error

也可以做如下修改:

// 使用字面量创建要给空map

colors := map[string]string{}

// 将red加入colors

colors["red"] = "#da137" // no panic or error

强烈推荐使用第二种,因为用字面量创建map比较简洁而且比较快

(2) 从映射获取值并判断键是否存在

在Go语言里,通过键来索引值时,即便这个键不存在也会返回一个值,有时候我们需要判断获取到的值是否时默认的零值,代码如下所示。

// 使用字面量创建一个空map

colors := map[string]string{}

// 将red加入colors

colors["red"] = "#da137"

// 获取blue对应的值并判断是否为零值

value1, exists := colors["blue"]

if exists {

fmt.Println(value1)

}

// 也可以通过值直接判断是否为零值

value2 := colors["blue"]

if value2 != "" {

fmt.Println(value2)

}

(3) 在函数间传递映射

package main

import (

"fmt"

"unsafe"

)

func foo(m map[string]string) {

// 打印参数的大小

fmt.Println("参数大小", unsafe.Sizeof(m))

// 删除键为green的元素

delete(m, "green")

}

func main() {

// 使用字面量创建一个空map

colors := map[string]string{}

// 将red加入colors

colors["red"] = "#da137"

colors["coral"] = "#ff7f50"

colors["green"] = "#228b22"

// 调用foo函数

foo(colors)

// 遍历打印map

for k, v := range colors {

fmt.Println("key=", k, "value=", v)

}

}

编译并运行以上代码,结果如下:

参数大小 8

key= red value= #da137

key= coral value= #ff7f50

Process finished with exit code 0

映射是引用类型的数据结构,在函数间传递映射的时候,不会拷贝映射底层的数据,从上面代码的运行结果可以看出,只传递了一个8字节大小的指针,实际上,map类型就是一个指针类型,函数对映射的操作都是通过这个指针间接进行的,因此对映射中数据的修改,其它引用到该映射的地方也能感知到。

(4) 修改映射中的结构体类型,必须整体赋值(!!!)

package main

import "fmt"

type User struct {

name string

age int

}

func main() {

m := make(map[int]User)

user := User{

name: "lioney",

age: 18,

}

// 将user加入到map中

m[1] = user

// 修改user

// 不能通过map引用直接修改!!!

//m[1].age = 2 // error: cannot assign to struct field m[1].age in map

// 必须整体替换

user.age = 2

m[1] = user

fmt.Println(m)

}

(5) 并发问题

Go语言内置的map不是并发安全的,若是在并发场景下以共享的形式访问map中的元素,必须加锁,要想使用并发安全的map,可以使用Go语言标准库中sync包下提供的map。

参考文献

William Kennedy等《Go语言实战》第四章相关内容

李文塔《Go语言核心编程》第一章相关内容

1460000021583152

我是lioney,年轻的后端攻城狮一枚,爱钻研,爱技术,爱分享。

个人笔记,整理不易,感谢阅读、点赞和收藏。

文章有任何问题欢迎大家指出,也欢迎大家一起交流后端各种问题!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值