浅谈Golang map使用与陷阱

简介: 浅谈Golang map使用与陷阱

前言

关于更多map底层原理剖析,请点击这一篇博文Golang底层原理剖析之map

map介绍

  1. map 读取某个值时 - 返回结果可以为 value,bool 或者 value。注意后者,在key不存在时,会返回value对应类型的默认值
  2. map 的 range 方法需要注意 - key,value 或者 key。注意后者,可以和slice的使用结合起来
  3. map 的底层相关的实现 - 串联 初始化、赋值、扩容、读取、删除 这五个常见实现的背后知识点,详细参考示例代码链接与源码

使用示例

package main
import "fmt"
func mapper() {
  // var mp map[string]int 不会初始化
  mp := make(map[string]int)
  mp["Tom"] = 10
  age, ok := mp["Tom"]
  fmt.Println(age, ok)
  age1 := mp["Tom"]
  age2 := mp["Tom2"]
  fmt.Println(age1, age2)
  for key, value := range mp {
    fmt.Println(key, value)
  }
  for key := range mp {
    fmt.Println(key, mp[key])
  }
  var data = []int{1, 3, 5}
  for key := range data {
    fmt.Println(key, data[key])
  }
}
// go tool compile -S map.go
func mapCompile() {
  m := make(map[string]int, 9)
  key := "test"
  m[key] = 1
  _, ok := m[key]
  if ok {
    delete(m, key)
  }
}
/*
  源码文件:runtime/map.go
  初始化:makemap
    1. map中bucket的初始化 makeBucketArray
    2. overflow 的定义为哈希冲突的值,用链表法解决
  赋值:mapassign
    1. 不支持并发操作 h.flags&hashWriting
    2. key的alg算法包括两种,一个是equal,用于对比;另一个是hash,也就是哈希函数
    3. 位操作 h.flags ^= hashWriting 与 h.flags &^= hashWriting
    4. 根据hash找到bucket,遍历其链表下的8个bucket,对比hashtop值;如果key不在map中,判断是否需要扩容
  扩容:hashGrow
    1. 扩容时,会将原来的 buckets 搬运到 oldbuckets
  读取:mapaccess
    1. mapaccess1_fat 与 mapaccess2_fat 分别对应1个与2个返回值
    2. hash 分为低位与高位两部分,先通过低位快速找到bucket,再通过高位进一步查找,最后对比具体的key
    3. 访问到oldbuckets中的数据时,会迁移到buckets
  删除:mapdelete
    1. 引入了emptyOne与emptyRest,后者是为了加速查找
*/

map常见使用过程中的问题

  1. map 的 range 操作 - key、value 都是值复制
  2. map 如何保证按key的某个顺序遍历? - 分两次遍历,第一次取出所有的key并排序;第二次按排序后的key去遍历(这时你可以思考封装map和slice到一个结构体中)?
  3. map 的使用上,有什么要注意的? - 遍历时,尽量只修改或删除当前key,操作非当前的key会带来不可预知的结果
  4. 从 map 的设计上,我们可以学到 - Go语言对map底层的hmap做了很多层面的优化与封装,也屏蔽了很多实现的细节,适用于绝大多数的场景;而少部分有极高性能要求的场景,就需要深入到hmap中的相关细节。

问题示例

package main
import (
  "fmt"
)
func main() {
  mapAddr()
  mapModify()
  mapReplace()
  mapSet()
  mapMap()
}
func mapAddr() {
  var mp = map[int]int{1: 2, 2: 3, 3: 4}
  fmt.Printf("%p\n", &mp)
  fmt.Println("map range 1 start")
  // Tip 注意range中的k是不能保证顺序的
  for k, v := range mp {
    // Tip: k,v 是为了遍历、另外开辟内存保存的一对变量,不是map中key/value保存的地址
    fmt.Println(&k, &v)
    // Tip: 修改k/v后,不会生效
    k++
    v++
    // Tip 如果在这里加上一个delete操作,在k=1时会删除k=2的元素,然后就直接跳过元素2,遍历到元素3
    // map的delete是安全的,不会存在race等竞争问题,这里用到的k,v是值复制,删除是针对hmap的操作
    // delete(mp, k)
    // 编译错误,value不可寻址,因为这个在内部是频繁变化的
    // fmt.Printf("%p", &mp[k])
  }
  fmt.Println(mp)
  fmt.Println("map range 1 end")
}
func mapModify() {
  // Tip: 如果map最终的size比较大,就放到初始化的make中,会减少hmap扩容带来的内容重新分配
  //var mp2 = make(map[int]int,1000)
  var mp2 = make(map[int]int)
  mp2[1] = 2
  mp2[2] = 3
  mp2[3] = 4
  fmt.Println("map range 2 start")
  for k, v := range mp2 {
    // Tip: 在range的过程中,如果不断扩容,何时退出是不确定的,是随机的,和是否需要sleep无关
    mp2[k+1] = v + 1
    // time.Sleep(10 * time.Millisecond)
  }
  fmt.Println(len(mp2))
  fmt.Println("map range 2 end")
}
// https://stackoverflow.com/questions/45132563/idiomatic-way-of-renaming-keys-in-map-while-ranging-over-the-original-map
func mapReplace() {
  o := make(map[string]string) // original map
  r := make(map[string]string) // replacement map original -> destination keys
  o["a"] = "x"
  o["b"] = "y"
  r["a"] = "1"
  r["b"] = "2"
  fmt.Println(o) // -> map[a:x b:y]
  // Tip: 因为o的k-v是在不断增加的,所以遍历何时结束是不确定的
  // 此时k可能为"1"或者"2",对应的r[k]不存在,
  //此时r这个map会创建key为1或2,value返回默认值空字符串 "",
  //所以结果会多一个key(r[k])为"",value为"x"或"y"的异常值
  for k, v := range o {
    o[r[k]] = v //想要实现<1,x><2,y>,但是结果是map[:x 1:x 2:y]
  }
  // Tip: 到这里,也许你会好奇,为什么每次运行的结果会不一致呢?map[:x 1:x 2:y]或者map[:y 1:x 2:y]
  // 1. 首先,我们要了解一点,遍历这个工作在hmap中是通过buckets进行的
  // 2. 因为多次运行的结果不一致,说明每一次运行时,分配的bucket是有随机的
  // 3. 仔细查看hmap这个结构体,我们不难发现hash0这个随机值,确认其对分配bucket的hash计算带来的影响
  // 4. 因为有hash0这个随机种子,所以遍历range是随机的
  delete(o, "a")
  delete(o, "b")
  fmt.Println(o)
}
func mapSet() {
  // 空struct是不占用空间的,将struct{}作为value,就可以作为set集合来用了
  var mp = make(map[string]struct{})
  _, ok := mp["test"]
  fmt.Println(ok)
}
func mapMap() {
  mp := make(map[string]map[string]string)
  //date:=mp["test"]
  //date["test"]="test" panic
  //make初始化的时候只会初始化第一层的map,而第二层的map是不会初始化的
  //一定要对第二层的map进行初始化
  mp["test"] = make(map[string]string)
  makeDate := mp["test"]
  makeDate["test"] = "test"
  fmt.Println(mp)
}


目录
相关文章
|
7月前
|
存储 Go
Golang底层原理剖析之map
Golang底层原理剖析之map
66 1
|
7月前
|
存储 Go 容器
【golang】对键值有顺序要求时,不要使用 map
【golang】对键值有顺序要求时,不要使用 map
129 0
|
7月前
|
缓存 安全 Go
浅谈Golang线程安全的sync.Map
浅谈Golang线程安全的sync.Map
95 0
|
安全 Cloud Native Go
需要提醒你关于 golang 中 map 使用的几点注意事项
需要提醒你关于 golang 中 map 使用的几点注意事项
|
3月前
|
Go
Golang语言之映射(map)快速入门篇
这篇文章是关于Go语言中映射(map)的快速入门教程,涵盖了map的定义、创建方式、基本操作如增删改查、遍历、嵌套map的使用以及相关练习题。
42 5
|
4月前
|
Java Serverless Go
Golang 开发函数计算问题之在 Golang 中避免 "concurrent map writes" 异常如何解决
Golang 开发函数计算问题之在 Golang 中避免 "concurrent map writes" 异常如何解决
|
6月前
|
Go
GOLANG MAP 查找
GOLANG MAP 查找
113 3
|
6月前
|
存储 Go 索引
GOLANG MAP 底层实现
GOLANG MAP 底层实现
|
7月前
|
存储 编译器 Go
Golang深入浅出之-掌握Go语言Map:初始化、增删查改与遍历
【4月更文挑战第21天】Go语言中的`map`提供快速的键值对操作,包括初始化、增删查改和遍历。初始化时,推荐使用`make()`函数,如`make(map[string]int)`。插入和查询键值对直接通过索引访问,更新则重新赋值。删除键值对需用`delete()`函数,确保键存在。遍历map常用`for range`,注意避免在遍历中修改map。了解这些并避免易错点,能提升代码效率和可读性。
127 1
Golang深入浅出之-掌握Go语言Map:初始化、增删查改与遍历
|
7月前
|
存储 缓存 安全
Golang深入浅出之-Go语言中的并发安全容器:sync.Map与sync.Pool
Go语言中的`sync.Map`和`sync.Pool`是并发安全的容器。`sync.Map`提供并发安全的键值对存储,适合快速读取和少写入的情况。注意不要直接遍历Map,应使用`Range`方法。`sync.Pool`是对象池,用于缓存可重用对象,减少内存分配。使用时需注意对象生命周期管理和容量控制。在多goroutine环境下,这两个容器能提高性能和稳定性,但需根据场景谨慎使用,避免不当操作导致的问题。
211 7