map定义

Go语言中 map的定义语法如下

    map[KeyType]ValueType

其中,

    KeyType:表示键的类型。ValueType:表示键对应的值的类型。

map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:

    make(map[KeyType]ValueType, [cap])
其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。

map基本使用

func main() {scoreMap := make(map[string]int, 8)scoreMap["张三"] = 90scoreMap["小明"] = 100fmt.Println(scoreMap)fmt.Println(scoreMap["小明"])fmt.Printf("type of a:%T\n", scoreMap)
}

输出:

    map[小明:100 张三:90]100type of a:map[string]int

map也支持在声明的时候填充元素,例如:

func main() {userInfo := map[string]string{"username": "pprof.cn","password": "123456",}fmt.Println(userInfo) //
}
/直接创建初始化一个map
var mapInit = map[string]string {"xiaoli":"湖南", "xiaoliu":"天津"}
//声明一个map类型变量,
//map的key的类型是string,value的类型是string
var mapTemp map[string]string
//使用make函数初始化这个变量,并指定大小(也可以不指定)
mapTemp = make(map[string]string,10)
//存储key ,value
mapTemp["xiaoming"] = "北京"
mapTemp["xiaowang"]= "河北"
//根据key获取value,
//如果key存在,则ok是true,否则是flase
//v1用来接收key对应的value,当ok是false时,v1是nil
v1,ok := mapTemp["xiaoming"]
fmt.Println(ok,v1)
//当key=xiaowang存在时打印value
if v2,ok := mapTemp["xiaowang"]; ok{fmt.Println(v2)
}
//遍历map,打印key和value
for k,v := range mapTemp{fmt.Println(k,v)
}
//删除map中的key
delete(mapTemp,"xiaoming")
//获取map的大小
l := len(mapTemp)
fmt.Println(l)

键值对是否存在

可以使用 val1 = map1[key1] 的方法获取 key1 对应的值 val1。如果 map 中不存在 key1,val1 就是一个值类型的空值。
这就会给我们带来困惑了:现在我们没法区分到底是 key1 不存在还是它对应的 value 就是空值。

为了解决这个问题,我们可以这么用:val1, isPresent = map1[key1]

isPresent 返回一个 bool 值:如果 key1 存在于 map1,val1 就是 key1 对应的 value 值,并且 isPresent 为 true;如果 key1 不存在,val1 就是一个空值,并且 isPresent 会返回 false。
如果你只是想判断某个 key 是否存在而不关心它对应的值到底是多少,你可以这么做:

_, ok := map1[key1] // 如果key1存在则ok == true,否则ok为false

map的遍历

Go语言中使用for range遍历map。

func main() {scoreMap := make(map[string]int)scoreMap["张三"] = 90scoreMap["小明"] = 100scoreMap["王五"] = 60for k, v := range scoreMap {fmt.Println(k, v)}
}

只需要key

但我们只想遍历key的时候,可以按下面的写法:

func main() {scoreMap := make(map[string]int)scoreMap["张三"] = 90scoreMap["小明"] = 100scoreMap["王五"] = 60for k := range scoreMap {fmt.Println(k)}
}

注意: 遍历map时的元素顺序与添加键值对的顺序无关。

map 默认是无序的,不管是按照 key 还是按照 value 默认都不排序

如下面的例子打印结果并不是按照key 1 2 3 4的顺序打印的,属于随机排序的,多次执行代码打印的顺序是不一致的

package main
import "fmt"func main() {map1 := make(map[int]float32)map1[1] = 1.0map1[2] = 2.0map1[3] = 3.0map1[4] = 4.0for key, value := range map1 {fmt.Printf("key is: %d - value is: %f\n", key, value)}
}

输出结果:

key is: 3 - value is: 3.000000
key is: 1 - value is: 1.000000
key is: 4 - value is: 4.000000
key is: 2 - value is: 2.000000

如果想遍历map,并且保证按照入队顺序打印出来,必须得借用切片

需要将 key(或者 value)拷贝到一个切片,再对切片排序(使用 sort 包),然后可以使用切片的 for-range 方法打印出所有的 key 和 value。

// the telephone alphabet:
package main
import ("fmt""sort"
)var (barVal = map[string]int{"alpha": 34, "bravo": 56, "charlie": 23,"delta": 87, "echo": 56, "foxtrot": 12,"golf": 34, "hotel": 16, "indio": 87,"juliet": 65, "kili": 43, "lima": 98}
)func main() {fmt.Println("unsorted:")for k, v := range barVal {fmt.Printf("Key: %v, Value: %v / ", k, v)}keys := make([]string, len(barVal))i := 0for k, _ := range barVal {keys[i] = ki++}sort.Strings(keys)fmt.Println()fmt.Println("sorted:")for _, k := range keys {fmt.Printf("Key: %v, Value: %v / ", k, barVal[k])}
}

输出结果:

unsorted:
Key: bravo, Value: 56 / Key: echo, Value: 56 / Key: indio, Value: 87 / Key: juliet, Value: 65 / Key: alpha, Value: 34 / Key: charlie, Value: 23 / Key: delta, Value: 87 / Key: foxtrot, Value: 12 / Key: golf, Value: 34 / Key: hotel, Value: 16 / Key: kili, Value: 43 / Key: lima, Value: 98 /
sorted:
Key: alpha, Value: 34 / Key: bravo, Value: 56 / Key: charlie, Value: 23 / Key: delta, Value: 87 / Key: echo, Value: 56 / Key: foxtrot, Value: 12 / Key: golf, Value: 34 / Key: hotel, Value: 16 / Key: indio, Value: 87 / Key: juliet, Value: 65 / Key: kili, Value: 43 / Key: lima, Value: 98 /

值为切片类型的map

map跟map的value切片都要初始化
下面的代码演示了map中值为切片类型的操作:

func main() {var sliceMap = make(map[string][]string, 3)//map初始化fmt.Println(sliceMap)fmt.Println("after init")key := "中国"value, ok := sliceMap[key]if !ok {value = make([]string, 0, 2)//map的value切片初始化}value = append(value, "北京", "上海")sliceMap[key] = valuefmt.Println(sliceMap)
}

Map实现原理

最通俗的话说Map是一种通过key来获取value的一个数据结构,其底层存储方式为数组,在存储时key不能重复,当key重复时,value进行覆盖,我们通过key进行hash运算(可以简单理解为把key转化为一个整形数字)然后对数组的长度取余,得到key存储在数组的哪个下标位置,最后将key和value组装为一个结构体,放入数组下标处,看下图:

   length = len(array) = 4hashkey1 = hash(xiaoming) = 4index1  = hashkey1% length= 0hashkey2 = hash(xiaoli) = 6index2  = hashkey2% length= 2

数据结构

实现原理
Go 语言使用拉链法来解决哈希碰撞的问题实现了哈希表,**它的访问、写入和删除等操作都在编译期间转换成了运行时的函数或者方法。**哈希在每一个桶中存储键对应哈希的前 8 位,当对哈希进行操作时,这些 tophash 就成为可以帮助哈希快速遍历桶中元素的缓存。
哈希表的每个桶都只能存储 8 个键值对,一旦当前哈希的某个桶超出 8 个,新的键值对就会存储到哈希的溢出桶中随着键值对数量的增加,溢出桶的数量和哈希的装载因子也会逐渐升高,超过一定范围就会触发扩容,扩容会将桶的数量翻倍,元素再分配的过程也是在调用写操作时增量进行的,不会造成性能的瞬时巨大抖动。

源码位于src\runtime\map.go 中。
go的map和C++map不一样,底层实现是哈希表,包括两个部分:hmap和bucket。
里面最重要的是buckets(桶),buckets是一个指针,最终它指向的是一个结构体:

// A bucket for a Go map.
type bmap struct {tophash [bucketCnt]uint8
}

但这只是表面(src/runtime/hashmap.go)的结构,编译期间会给它加料,动态地创建一个新的结构:

type bmap struct {topbits  [8]uint8keys     [8]keytypevalues   [8]valuetypepad      uintptroverflow uintptr
}

每个bucket固定包含8个key和value(可以查看源码bucketCnt=8).实现上面是一个固定的大小连续内存块,分成四部分:每个条目的状态,8个key值,8个value值,指向下个bucket的指针。
创建哈希表使用的是makemap函数.map 的一个关键点在于,哈希函数的选择。在程序启动时,会检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。这是在函数 alginit() 中完成,位于路径:src/runtime/alg.go 下。

map查找就是将key哈希后得到64位(64位机)用最后B个比特位计算在哪个桶。在 bucket 中,从前往后找到第一个空位。这样,在查找某个 key 时,先找到对应的桶,再去遍历 bucket 中的 key。
关于map的查找和扩容可以参考map的用法到map底层实现分析。

type hmap struct {count     int //map元素的个数,调用len()直接返回此值// map标记:// 1. key和value是否包指针// 2. 是否正在扩容// 3. 是否是同样大小的扩容// 4. 是否正在 `range`方式访问当前的buckets// 5. 是否有 `range`方式访问旧的bucketflags     uint8 B         uint8  // buckets 的对数 log_2noverflow uint16 // overflow 的 bucket 近似数hash0     uint32 // hash种子 计算 key 的哈希的时候会传入哈希函数buckets   unsafe.Pointer // 指向 buckets 数组,大小为 2^B 如果元素个数为0,就为 nil// 扩容的时候,buckets 长度会是 oldbuckets 的两倍oldbuckets unsafe.Pointer // bucket slice指针,仅当在扩容的时候不为nilnevacuate  uintptr // 扩容时已经移到新的map中的bucket数量extra *mapextra // optional fields
}

hash冲突

数组一个下标处只能存储一个元素,也就是说一个数组下标只能存储一对key,value, hashkey(xiaoming)=4占用了下标0的位置,假设我们遇到另一个key,hashkey(xiaowang)也是4,这就是hash冲突(不同的key经过hash之后得到的值一样),那么key=xiaowang的怎么存储? hash冲突的常见解决方法
开放定址法:也就是说当我们存储一个key,value时,发现hashkey(key)的下标已经被别key占用,那我们在这个数组中空间中重新找一个没被占用的存储这个冲突的key,那么没被占用的有很多,找哪个好呢?常见的有线性探测法,线性补偿探测法,随机探测法,这里我们主要说一下线性探测法

线性探测,字面意思就是按照顺序来,从冲突的下标处开始往后探测,到达数组末尾时,从数组开始处探测,直到找到一个空位置存储这个key,当数组都找不到的情况下回扩容(事实上当数组容量快满的时候就会扩容了);查找某一个key的时候,找到key对应的下标,比较key是否相等,如果相等直接取出来,否则按照顺寻探测直到碰到一个空位置,说明key不存在。如下图:首先存储key=xiaoming在下标0处,当存储key=xiaowang时,hash冲突了,按照线性探测,存储在下标1处,(红色的线是冲突或者下标已经被占用了) 再者key=xiaozhao存储在下标4处,当存储key=xiaoliu是,hash冲突了,按照线性探测,从头开始,存储在下标2处 (黄色的是冲突或者下标已经被占用了)

拉链法:何为拉链,简单理解为链表,当key的hash冲突时,我们在冲突位置的元素上形成一个链表,通过指针互连接,当查找时,发现key冲突,顺着链表一直往下找,直到链表的尾节点,找不到则返回空,如下图:

开放定址(线性探测)和拉链的优缺点:

- 由上面可以看出拉链法比线性探测处理简单
- 线性探测查找是会比拉链法更消耗时间
- 线性探测会更加容易导致扩容,而拉链不会
- 拉链存储了指针,所以空间上会比线性探测占用多一点
- 拉链是动态申请存储空间的,所以更适合链长不确定的

map与并发

map不是并发写安全的,不支持并发读写。如果对map实例进行并发读写,程序运行时会发生panic。
如果仅是并发读没有写,map是没问题的,即当你确定任意时刻这个map被读的时候,不会被写,那就没问题,但是,这个基本无法保证!

package mainimport ("fmt""time"
)// chapter3/sources/map_concurrent_read_and_write.gofunc doIteration(m map[int]int) {for k, v := range m {_ = fmt.Sprintf("[%d, %d] ", k, v)}
}
func doWrite(m map[int]int) {for k, v := range m {m[k] = v + 1}
}func main() {m := map[int]int{1: 11,2: 12,3: 13,}go func() {for i := 0; i < 1000; i++ {doIteration(m)}}()go func() {for i := 0; i < 1000; i++ {doWrite(m)}}()time.Sleep(5 * time.Second)
}


我们会得到上述panic信息**。如果仅仅是并发读,则map是没有问题的。**

package mainimport ("fmt""time"
)// chapter3/sources/map_concurrent_read_and_write.gofunc doIteration(m map[int]int) {for k, v := range m {_ = fmt.Sprintf("[%d, %d] ", k, v)}
}
func doWrite(m map[int]int) {for k, v := range m {m[k] = v + 1}
}func main() {m := map[int]int{1: 11,2: 12,3: 13,}go func() {for i := 0; i < 1000; i++ {doIteration(m)}}()//将并发写注释掉,再次运行程序就没问题了// go func() {//  for i := 0; i < 1000; i++ {//      doWrite(m)//  }// }()time.Sleep(5 * time.Second)
}

Go 1.9版本中引入了支持并发写安全的sync.Map类型,可以用来在并发读写的场景下替换掉map。

要点

  • 不要依赖map的元素遍历顺序;
  • map不是线程安全的,不支持并发读写;
  • 不要尝试获取map中元素(value)的地址;因为 map 的 value 本身是不可寻址
  • 尽量使用cap参数创建map,以提升map平均访问性能,减少频繁扩容带来的不必要损耗

注意事项:

float 类型可以作为 map 的 key 吗

可以,但是尽量不要使用!
详见

map可以使用 len(),不能 使用cap()

make创建map尽可能的为其指定cap

如果初始创建map时没有创建足够多可以应付map使用场景的bucket,那么随着插入map元素数量的增多,map会频繁扩容,而这一过程将降低map的访问性能
如果可能的话,我们最好对map使用规模做出粗略的估算,并使用cap参数对map实例进行初始化。下面是使用cap参数与不使用map参数的map写性能基准测试及测试结果:

// chapter3/sources/map_test.goconst mapSize = 10000func BenchmarkMapInitWithoutCap(b *testing.B) {for n := 0; n < b.N; n++ {m := make(map[int]int)for i := 0; i < mapSize; i++ {m[i] = i}}
}func BenchmarkMapInitWithCap(b *testing.B) {for n := 0; n < b.N; n++ {m := make(map[int]int, mapSize)for i := 0; i < mapSize; i++ {m[i] = i}}
}$go test -benchmem -bench=. map_test.go
goos: darwin
goarch: amd64
BenchmarkMapInitWithoutCap-8   2000   645946 ns/op   687188 B/op   276 allocs/op
BenchmarkMapInitWithCap-8      5000   317212 ns/op   322243 B/op   11 allocs/op
PASS
ok      command-line-arguments  2.987s

可以看出,使用cap参数的map实例的平均写性能是不使用cap参数的2倍。

map 的 value 是不可寻址的

演示错误使用

package mainimport "fmt"type Math struct {x, y int
}var m = map[string]Math{"foo": Math{2, 3},
}func main() {m["foo"].x = 4//这里会报错fmt.Println(m["foo"].x)
}

运行会报错:

D:\GOMOD\rpcdemo>go run main.go
# command-line-arguments
.\main.go:14:2: cannot assign to struct field m["foo"].x in map

错误原因:

对于类似 X = Y的赋值操作,必须知道 X 的地址,才能够将 Y 的值赋给 X,但 go 中的 map 的 value 本身是不可寻址的。
究其原因,因为Go的map是通过散列表来实现的,说得更具体一点,就是通过数组和链表组合实现的。并且Go的map也可以做到动态扩容,当进行扩容之后,map的value那块空间地址就会产生变化,所以无法对map的value进行寻址。

解决方案

使用临时变量

package mainimport "fmt"type Math struct {x, y int
}var m = map[string]Math{"foo": Math{2, 3},
}func main() {tmp := m["foo"]tmp.x = 4m["foo"] = tmpfmt.Println(m["foo"].x)
}
D:\GOMOD\rpcdemo>go run main.go
4

修改数据结构

package mainimport "fmt"type Math struct {x, y int
}var m = map[string]*Math{"foo": &Math{2, 3},
}func main() {m["foo"].x = 4fmt.Println(m["foo"].x)fmt.Printf("%#v", m["foo"]) // %#v 格式化输出详细信息
}
D:\GOMOD\hello>go run hello.go
4
&main.Math{x:4, y:3}

视频教程截图

在迭代过程中,如果创建新的键值对,那么新增键值对,可能被迭代,也可能不会被迭代。

package mainimport "fmt"func main() {m := map[string]int{"one":   1,"two":   2,"three": 3,}for k, v := range m {delete(m, "two")//迭代过程中,删除还未迭代到的键值对,则该键值对不会被迭代。m["four"] = 4//可能被迭代,也可能不被迭代fmt.Printf("%v: %v\n", k, v)}
}

多次运行结果:

D:\GOMOD\sheji_shixian\first>go run test.go
two: 2
three: 3
one: 1
D:\GOMOD\sheji_shixian\first>go run test.go
one: 1
four: 4
three: 3
aa: 4

GO map(集合)相关推荐

  1. Java中的Map集合遍历总结(详尽版)

    因为Map集合中的键值对排列无序,所以不能用传统的for循环来遍历,只能使用加强循环(for-each)和迭代器进行遍历. 让我们通过例子来了解Map集合的遍历: package gather; im ...

  2. 安卓取map集合转换为json_android json解析成map格式

    "discount": { "3": "34", "4": "33", "5": ...

  3. Map集合中value()方法与keySet()、entrySet()区别 ——转载

    为什么80%的码农都做不了架构师?>>>    在Map集合中 values():方法是获取集合中的所有的值----没有键,没有对应关系, KeySet(): 将Map中所有的键存入 ...

  4. java map 队列_Java:queue队列,map集合

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 Queue: 基本上,一个队列就是一个先入先出(FIFO)的数据结构 Queue接口与List.Set同一级别,都是继承了Collection接口.Lin ...

  5. 8.Map集合(HashMapTreeMap)

    一.Map集合概述和使用 1.Map集合概述 Interface Map<K,V> K:键的类型 V:值的类型 将键映射到值的对象:不能包含重复的键:每个键可以映射到最多一个值 创建Map ...

  6. Map集合练习之对字符串中字母出现的次数求和

    代码需求 如有这么一个字符串 String str = "fdg+avAdc bs5dDa9c-dfs"; MapTest.java package zhouls.bigdata. ...

  7. 【mybatis】mybatis中 返回map集合

    关于mybatis返回map集合的操作: 1.mapper.xml中写一个查询返回map的sql <select id="findMap" parameterType=&qu ...

  8. Map集合的遍历(java)

    2019独角兽企业重金招聘Python工程师标准>>> Map集合通过entrySet 和 keySet都可以使用迭代器 以及for循环拿到key和value: import jav ...

  9. java map key是否存在_java中如何判断map集合中是否存在key

    有两种方法可以判断map集合中是否存在某个key. 方法1:直接使用java api提供的containsKey(): 方法2:循环遍历,逐个比较. java相关视频推荐:java视频 具体实现代码如 ...

  10. Map集合遍历的四种方式理解和简单使用

    Map集合遍历的四种方式理解和简单使用 ~Map集合是键值对形式存储值的,所以遍历Map集合无非就是获取键和值,根据实际需求,进行获取键和值 1:无非就是通过map.keySet()获取到值,然后根据 ...

最新文章

  1. Xcode 11 新建项目适配 iOS 13 以下设备
  2. java web windows_WinSW让你的JavaWEB程序作为Windows服务启动!
  3. JS与PHP向函数传递可变参数的区别
  4. Java动态规划求最长公共子序列(LCS)
  5. @Transaction注解详解
  6. css preserve-3d 使用
  7. 浅谈测试环境管理方式
  8. 《Blender图解教程:新手入门练习》
  9. 记一次笔记本更换固态硬盘事件
  10. java中使用length获取二维数组的长度
  11. Super-Auto-Refresh_v2.5.1实现谷歌浏览器定时刷新
  12. 嵌入式linux学习笔记(一)
  13. 有道难题2010有道谜题标准答案
  14. 为chrome书签栏中,没有默认图标的网站添加图标
  15. iOS 点击提示框视图以外的其他地方时隐藏弹框
  16. 自建网上商城平台该如何做好运营?
  17. 在微型计算机中应用最普遍的数字编码是,计算机基础知识理论复习题及答案
  18. 国家版权局:多部门联合打击春节档院线电影盗版传播
  19. mysql数据转换拼音函数
  20. 教育行业如何选呼叫中心系统

热门文章

  1. 树莓派2使用酷芯微C201-D图传模块的使用总结
  2. ICT分析选点调试,ict治具技术培训
  3. [Reprint] 常用素数
  4. ArrayList做的员工薪资管理系统
  5. 零成本拓客秘籍丨券商玩转社群营销的5个步骤
  6. Java创新创业讲座心得体会_基于Java?Web的创新创业管理系统设计与实现
  7. 新入职了一个00后卷王,天天加班12点,太让人崩溃......
  8. 启发式算法Python实现(一) 模拟退火
  9. 青海师范大学计算机技术调剂,青海师范大学2017年考研调剂信息
  10. SimLab Composer 9 for Mac(3D场景渲染工具)