golang——map

目录

1.map特点

2.map声明和初始化

变量初始化

3.map常见操作

4.map的嵌套

5.golang中map底层结构

6.map遍历为什么是无序的

7.map为什么是非线程安全的

8.实现map线程安全

9.map如何查找

10.map的key可以使用那些类型

11.map冲突的解决方式

12.go map的负载因子为什么是6.5

13.go map如何扩容

14.sync.map和map对比


1.map特点

以键值对为元素的数据集合

特点:

1.键不能重复

2.键必须可哈希(int/bool/float/string/array)

3.无序

快:可以直接根据键值找到数据存储的位置,而不用从前到后一一比对

2.map声明和初始化

package main

import "fmt"

func main() {
	userInfo := map[string]string{}
	userInfo1 := map[string]string{"name": "xxx", "age": "18"}
	//新增键值
	userInfo["name"] = "000"
	//修改键值
	userInfo1["name"] = "111"
	fmt.Println(userInfo)
	fmt.Println(userInfo1)

   
}

用make进行初始化

package main

import "fmt"

func main() {
	userInfo2 := make(map[string]string)
	//初始化容量cap为10
	userInfo3 := make(map[string]string,10)
	userInfo2["name"] = "222"
	fmt.Println(userInfo2)
	fmt.Println(userInfo3)

	v1 := make(map[[2]int]float32)
	v1[[2]int{1, 1}] = 1.4
	v1[[2]int{1, 3}] = 5.3
	fmt.Println(v1)

    v2 := make(map[[2]int][3]float32)
	v2[[2]int{1, 1}] = [3]float32{1.0, 2.1, 3.5}
	v2[[2]int{1, 3}] = [3]float32{1.0, 2.1, 3.5}
	fmt.Println(v2)
}

注意:

  • 只声明map,维护了一个nil,添加键值时会报错  panic: assignment to entry in nil map  

        一般只用于整体赋值

package main

func main() {
	var userInfo map[string]string
	//只声明,无法添加键值对
	userInfo["name"] = "000"

}
package main

func main() {
	userInfo0 := map[string]string{}
	var userInfo map[string]string
	userInfo=userInfo0
	//只声明,无法添加键值对
	userInfo["name"] = "000"

}

       整体赋值时,userInfo0和userInfo始终指向同一个地址,不会像切片一样因为扩容导致指向同一个地址

  • 用new分配内存返回的是指针,添加键值时会报错

        一般只用于整体赋值

package main

import "fmt"

func main() {
	userInfo := new(map[string]string)
	userInfo1 := map[string]string{}
	//userInfo["name"] = "xxx"  报错
	userInfo = &userInfo1
	fmt.Println(userInfo)
}

变量初始化

变量初始化=变量声明+变量内存分配

make和new主要是用来分配内存的

var是用来声明变量的

var值声明

  • 值类型变量

系统会默认为他分配内存空间,并赋该类型的零值

  • 指针类型/引用类型变量

系统不会为它分配内存,默认为nil,如果直接使用,系统会抛异常,必须进行内存分配才能使用

make和new

make和new是内置函数,不是关键字

二者都是分配空间

  • 但是new返回值类型的指针
  • make返回的是类型本身

  • make只能用于slice、map、chan的初始化
  • new可以分配任意类型的数据,并且置零

  • new分配的空间会被清零
  • make则会初始化

3.map常见操作

获取长度&容量


m := make(map[string]string, 10)
获取长度,即存储的键值对个数
len:=len(m)

获取容量,cap为根据参数值10计算出的合适容量
cap:=cap(m) 不支持,会报错

删除

delete(userInfo,"name")

  • 单个——m[key]
  • 全部——循环

4.map的嵌套

值嵌套

值可为任意类型

package main

func main() {
	v1:=make(map[string][2]map[int]int)
	v2:=make(map[string][]int)
	
	v1["xx"]=[2]map[int]int{map[int]int{1:2,2:3},map[int]int{1:3,4:5}}
	v2["xx"]=[]int{1,2,3}
	

}

键嵌套

键不可重复,且键必须可哈希

不可哈希类型不可内嵌在键中

package main

func main() {
	v1 := make(map[[2]int]int)
	v1[[2]int{1,2}]=1

}

5.golang中map底层结构

golang——map_第1张图片

  1. hmap(哈希表,含指向桶数组的指针)
  2. bmap(桶)
  3. mapextra(溢出桶)

Go中的map是一个指针,指向hmap结构体

hmap是哈希表,hamp通过buckets指针指向结构为bmap的数组,每个bmap底层都采用链表结构,

bmap,每一个桶最多存8个键值对,如果放满8个,通过bmap内的overflow指针指向溢出桶

golang——map_第2张图片

根据key计算出的hash值

  • 低B位决定放入哪个桶,同一个桶内哈希结果的低B位相同
  • 高8位决定key落入桶内的哪个位置(1-8)
type hmap struct{
    //哈希表中元素个数,调用len(map)时,返回的就是该字段
    count int
    //状态标志(是否处于正在写入的状态等,如果有线程正在写入,就不能读写)
    flags uint8
    //buckets(桶)的对数
    //如果B=5,buckets桶的长度=2^B=32,有32个桶
    B uint8
    //溢出桶的数量
    noverflow uint16
    //生成哈希的随机种子
    hash0 uint32
    //指向buckets数组的指针,数组大小为2^B,如果元素个数为0,指向nil
    buckets unsafe.Pointer
    //如果发生扩容,oldbuckets指向老的buckets数组,老的buckets数组大小是新的的1/2,非扩容状态下为nil
    oldbuckets unsafe.Pointer
    //表示扩容进度,小于此地址的buckets代表已经搬迁完成
    nevacuate uintptr
    //存储溢出桶,为了优化GC扫描而设计
    extra *mapextra
}

type bmap struct{
    tophash [bucketCnt]uint8
    //len为8的数组
    //用来快速定位key是否在这个bmap中
    //一个桶最多8个槽位,如果key所在的tophash值在tophash中,则代表该key在这个桶中
}

在编译过程中runtime.bmap会拓展成以下结构体

type bmap struct{
    tophash [8]uint8
    //keytype由编译器编译时确定
    keys [8]keytype
    //elemtype由编译器编译时确定
    values [8]elemtype
    //overflow指向下一个bmap,overflow是uintptr而不是*bmap类型,是为了减少gc,溢出桶存储到extra字段中
    overflow uintptr
}

tophash

用于实现快速定位key的位置,存储哈希值的高8位,比对时会先比较高8位

存储:

  1. 在实现过程中会使用key的hash值的高8位作为tophash值
  1. tophash字段不仅存储key哈希值的高8位,还会存储一些状态值,用来表明当前桶单元状态,这些状态值都是小于minTopHash的,为了避免key哈希值的高8位值和这些状态值相等,产生混淆情况,所以当key哈希值高8位小于minTopHash时,自动将其值加上minTopHash作为该key的TopHash

key和value是各自放在一起的,并不是k/v/k/v这种形式,当key和value类型不一样时,key和value占用的字节大小不一样,k/v这种形式,可能会因为内存对齐而导致空间浪费,go这种形式将key和value分开存储,更加节省内存空间

初始化过程

初始化一个可容纳10个元素的map

info := make(map[string]string,10)

Step1.创建一个hmap结构体对象

Step2.生成一个哈希因子hash0并赋值到hmap对象中(用于后续为key创建哈希值)

Step3.根据hint=0,并根据算法规则来创建B,当前B应该为1

hint        B

0~8        0

9~13      1

14~26    2

Step4.根据B去创建桶(bmap对象)并存放在buckets数组中,当前bmap的数量应为2

当B<4时,根据B创建桶的个数的规则为:2^B(标准桶

当B≥4时,根据B创建桶的个数的规则为:2^B+2^(B-4) (标准桶+溢出桶

写入数据过程

info["name"]="xxx"

Step1.结合hash因子hash0和键生成哈希值

Step2.获取哈希值的后B位,并根据后B位的值来决定此键值对存入到哪个桶中

Step3.在上一步确定桶之后,接下来在桶中写入数据

Step4.hmap的个数count++(map中的元素个数+1)

读取数据过程

value := info["name"]

Step0.写保护监测,如果flag标志位被置1,说明有其他协程正在“写”,从而导致程序panic

Step1.结合哈希因子和键生成哈希值

Step2.获取哈希值的后B位,并根据后B位的值决定将此键值对存放到哪个桶中

如果当前正在扩容中,并且定位到的旧bucket数据还未完成迁移,则使用旧的bucket(扩容前的bucket)

Step3.确定桶之后,再根据key的哈希值计算出tophash(哈希值高8位),根据tophash和key值去桶中查找数据,如果不在,需要到overflow中查找

golang——map_第3张图片

扩容过程

向map中添加数据时,当达到某个条件,则会引发字典扩容。

扩容条件:

  • map中 数据总个数/桶个数(负载因子) >6.5,引发翻倍扩容
  • 使用了太多的溢出桶时(溢出桶使用的太多会导致map处理速度降低)

        B≤15,已使用的溢出桶个数≥2^{B}时,引发等量扩容

        B>15,已使用的溢出桶个数≥2^{15}时,引发等量扩容

扩容后oldbuckets指向旧桶

迁移

即将旧桶中数据迁移到新桶中

翻倍扩容:将旧桶中的数据分流至新的两个桶中(比例不定),并且桶编号位置为:同编号位置和翻倍后对应编号位置

等量扩容(溢出桶太多引发的扩容):将旧桶(含溢出桶)的值迁移到新桶中

意义:当溢出桶比较多而每个桶中数据不多,可以通过等量扩容和迁移让数据更加紧凑,从而减少溢出桶

6.map遍历为什么是无序的

使用range多次遍历map值时输出的key和value的顺序可能不同->开发者有意为之

原因:

  1. map在遍历时,并不是从固定的0号bucket开始遍历,而是从一个随机序号的bucket,再从其中随机的cell开始遍历
  2. map遍历时,是按序遍历bucket,同时按序遍历overflow bucket中的cell,但是map在扩容后,会发生key的搬迁,造成原来落在一个bucket中的key,搬迁后,有可能落到其他bucket中

如果想顺序遍历map,需要对map key先排序,再按照key的顺序遍历map

package main

import (
	"fmt"
	"sort"
)

func main() {
	fmt.Println("first range:")
	m := map[int]string{1: "a", 2: "b", 3: "c"}
	for i, v := range m {
		fmt.Printf("m[%v]=%v\n", i, v)
	}
	fmt.Println("second range:")

	var sl []int
	for i := range m {
		sl = append(sl, i)
	}
	sort.Ints(sl)
	for _, v := range sl {
		fmt.Printf("m[%v]=%v\n", v, m[v])
	}

}

7.map为什么是非线程安全的

考虑map更应适配典型使用场景,而不是为了小部分情况(并发访问),导致大部分程序付出加锁代价,决定不支持

map默认是并发不安全的,同时对map进行并发读写时,程序会出现错误

fatal error:concurrent map writes,程序panic

package main

import "fmt"

func main() {
	s := make(map[int]int)
	for i := 0; i < 100; i++ {
		go func(i int) {
			s[i] = i
		}(i)
	}
	for i := 0; i < 100; i++ {
		go func(i int) {
			fmt.Printf("map第%d个元素值是%d", i, s[i])
		}(i)
	}

}

golang——map_第4张图片

8.实现map线程安全

两种方式:

  • 读写锁map+sync.RWMutex
package main

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

func main() {
	var lock sync.RWMutex
	s := make(map[int]int)
	for i := 0; i < 100; i++ {
		go func(i int) {
			lock.Lock()
			s[i] = i
			lock.Unlock()
		}(i)
	}
	for i := 0; i < 100; i++ {
		go func(i int) {
			lock.RLock()
			fmt.Printf("map第%d个元素值是%d\n", i, s[i])
			lock.RUnlock()
		}(i)
	}
	//防止没执行完就退出了
	time.Sleep(time.Second * 1)

}
  • 使用Go提供的sync.map
package main

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

func main() {

	var s sync.Map
	for i := 0; i < 100; i++ {
		go func(i int) {
			s.Store(i, i)
		}(i)
	}
	for i := 0; i < 100; i++ {
		go func(i int) {
			load, _ := s.Load(i)
			fmt.Printf("map第%d个元素值是%d\n", i, load)
		}(i)
	}
	//防止没执行完就退出了
	time.Sleep(time.Second * 1)

}

9.map如何查找

Go读取map有两种语法

m := map[int]string{1: "a", 2: "b", 3: "c"}
  • 不带comman

不存在时,返回value类型的零值

v := m[1]
fmt.Println(v)
  • 带comman

ok为bool值,表示是否存在

v, ok := m[2]
fmt.Println(v, ok)

10.map的key可以使用那些类型

必须是可比较类型,数组、chan、指针等

不可比较slice、func、map

11.map冲突的解决方式

常见解决方法:

  1. 链地址法(尾插)
  2. 开放寻址法

从发生冲突的那个单元起,按照一定的次序,从哈希表中寻找一个空闲的单元,然后把发生冲突的元素存入到该单元。开放寻址法需要的表长度要大于等于所需要存放的元素数量

  • 线性探测法
  • 平方探测法
  • 随机探测法
  • 双重哈希法

golang——map_第5张图片

Go——链地址法,插入key

12.go map的负载因子为什么是6.5

负载因子=哈希表存储元素个数/桶个数

  1. 扩容

当程序运行时,出现负载因子过大,需要扩容,解决bucket过大的问题

  1. 迁移

当程序运行时,会不断地进行插入、删除等,会导致bucket不均,内存利用率低,需要迁移

参考各负载因子下的

  1. 溢出率
  2. 平均每对key/value的开销字节数
  3. 查找一个存在的key时,要查找的平均个数
  4. 查找一个不存在的key时,要查找的平均个数

当装载因子越大,填入元素越多,空间利用率就越高,发生哈希冲突的几率就越大

当负载因子越小,填入元素越少,冲突发生的几率减小,但空间浪费更多,还会提高扩容操作的次数

go选择了一个相对适中的值

13.go map如何扩容

  • 扩容时机

在向map插入新key时,会进行条件检测,符合下面这两个条件,就会触发扩容

if !h.growing() && (overLoadFactor(h.count+1,h.8)|| tooManyOverflowBuckets(h.noverflow,h.8){

}
//判断是否在扩容                    
func(h *heap)growing()bool{
   return h.oldbuckets!=nil 
}
  • 扩容条件

(1)超过负载

func overLoadFactor(count int,B uint8)bool{
    return count > bucketCnt && uintptr(count)>loadFactor*bucketShift(8)
}
bucketCnt = 8 一个桶可以装的最大元素个数
loadFactor =6.5 负载因子,平均每个桶的元素个数
bucketShift(8) 桶的个数

(2)溢出桶太多

当桶总数<2^15时,如果溢出桶总数≥桶总数,则认为溢出桶过多

当桶总数≥2^15时,直接与2^15比较,当溢出桶总数≥2^15时,即认为溢出桶太多

  • 扩容机制

双倍扩容

等量扩容

golang——map_第6张图片

渐进式哈希——最多每次只搬迁2个bucket

14.sync.map和map对比

type Map struct{
    mu Mutex
    read atomic.Value
    dirty map[interface{}]*entry
    misses int
}

对比原始map,和原始map+RWLock实现并发的方式,减小了加锁对性能的影响

采用了空间换时间的机制,冗余了两个数据结构

  1. read 负责读取(无锁)
  2. dirty 负责写入(写入dirty,并且更新read)

他做了一些优化:

可以无锁访问read map,优先操作read map 如果只操作read map就可以满足要求,那么就不用去操作write map(dirty),所以某些特定场景发生锁竞争的频率会远远小于map+RWLock的实现方式

优点:

适合读多写少的场景

缺点:

写多的场景,会导致read map缓存失效,需要加锁,冲突变多,性能急剧下降

你可能感兴趣的:(golang,golang)