浅谈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)
}

Supongo que te gusta

Origin blog.csdn.net/qq_42956653/article/details/121133395
Recomendado
Clasificación