Go语言map的使用及详解

Go语言map的使用及详解,第1张

1、Map基础:

Map是无序的键值对集合,是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。

package main

import "fmt"

func main() {
	//定义Map,不初始化,m值为空(nil),nil map 不能用来存放键值对
	var m map[string]string
	fmt.Println(m, nil == m) // map[] true

	//定义Map,初始化方法1,m1值不为nil
	var m1 map[string]string
	m1 = make(map[string]string)
	fmt.Println(m1, nil == m1) // map[] false

	//定义Map,初始化方法2,m1值不为nil
	m2 := make(map[string]string)
	m2["age"] = "31"
	m2["name"] = "yangfei"
	m2["address"] = "中国西安雁塔区"
	m2["school"] = "yulin"

	for k, v := range m2 {
		fmt.Println(fmt.Sprintf("k=%s,v=%s", k, v)) // 该打印结果是无序的
	}

	//初定义Map,方法3
	m3 := map[string]string{}
	fmt.Println(m3, nil == m3) // map[] false
	m3 = map[string]string{"key1": "1", "key2": "22", "key3": "333", "key4": "4444"}
	fmt.Println("删除前:m3=", m3) //删除前:m3= map[key1:1 key2:22 key3:333 key4:4444]
	delete(m3, "key3")
	fmt.Println("删除后……:m3=", m3) //删除后……:m3= map[key1:1 key2:22 key4:4444]
	for k, v := range m3 {
		delete(m3, k)
		//遍历过程中,可以delete元素,k,v为副本,所以值还在
		fmt.Println(fmt.Sprintf("k=%s,v=%s, m3=%v", k, v, m3))
		//k=key1,v=1, m3=map[key2:22 key4:4444]
		//k=key2,v=22, m3=map[key4:4444]
		//k=key4,v=4444, m3=map[]
	}

    //遍历过程中,可以delete元素:m3= map[]
	fmt.Println("遍历过程中,可以delete元素:m3=", m3) 
}
2、Map定义及注意点

var a map[int]int 这样对Map声明之后,仅仅创建了nil Map,是不能存储键值的,赋值会报错。

// 只有赋个初值,或者make实例化后,才可以存储键值对。
a = map[int]int{} 或者 a = make(map[int]int) 

map是引用类型,在函数中传递,是可以修改原内容的。

delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key 

delete(a, "key1")

3、Key和Value可以使用什么类型?

Key :只要是可比较(可以使用==进行比较,两边的 *** 作数可以相互赋值)的类型就可以,像整形,字符串类型,浮点型,布尔,接口,信道(chan),数组(必须类型相同);而map,slice和function不能作为Key的类型

Value :任何类型都可以。

这种写法见过吗?

func main() {
	var a map[bool]int
	a = make(map[bool]int)
	a[false] = 2
	fmt.Println(a) //map[false:2]
}

 key可以是数组,不能是切片,如下:

func main() {
	a := make(map[[4]string][]int)
	x := [4]string{"a"}
	y := []int{1, 2, 2}
	a[x] = y
	fmt.Println(a) // map[[a   ]:[1 2 2]]

	// 下面代码会报错:
    // Invalid map key type: must not be must not be a function, map,or slice
	b := make(map[[]string][]int)
}

key也可以是接口类型,也可以是信道chan类型,如下:

type Aa interface {
	GetName()
}

func main() {
	b := make(map[interface{}]interface{})
	var a Aa
	b[a] = 44
	fmt.Println(b) // map[:44]
	b["aa"] = 55
	fmt.Println(b) // map[:44 aa:55]

	c := make(map[chan int]string)
	x := make(chan int, 1)
	x <- 4
	c[x] = "yht"
	fmt.Println(c) // map[0xc00008e000:yht]
}
4golang map是安全的吗

golang中Map是不安全的(是线程不安全的),在同一时间段内,让不同 goroutine 中的代码,对同一个map进行读写 *** 作是不安全的,字典值本身可能会因这些 *** 作而产生混乱。

有两种办法解决:

4.1、加锁:

下面展示加读写锁处理:

import "sync"

type SafeMap struct {
	Data map[string]int
	Lock sync.RWMutex
}

func (m *SafeMap) Get(k string) int {
	m.Lock.RLock()
	defer m.Lock.RUnlock()
	return m.Data[k]
}

func (m *SafeMap) Set(k string, v int) {
	m.Lock.Lock()
	defer m.Lock.Unlock()

	m.Data[k] = v
}

定义一个安全map:

	mySafeMap := SafeMap{
		Data: make(map[string]int),
	}

4.2、用sync.Map

Go 1.9 中正式加入了并发安全的字典类型sync.Map。这个字典类型提供了一些常用的键值存取 *** 作方法,并保证了这些 *** 作的并发安全。

同时,它的存、取、删等 *** 作都可以基本保证在常数时间内执行完毕。换句话说,它们的算法复杂度与map类型一样都是O(1)的。在有些时候,与单纯使用原生map和互斥锁的方案相比,使用sync.Map可以显著地减少锁的争用。sync.Map本身虽然也用到了锁,但是,它其实在尽可能地避免使用锁。

import (
	"fmt"
	"sync"
)

func SyncMap() {
	var ma sync.Map                // 该类型是开箱即用,只需要声明既可
	ma.Store("key", "value")       // 存储值
	ma.Delete("key")               //删除值
	ma.LoadOrStore("key", "value") // 获取值,如果没有则存储
	fmt.Println(ma.Load("key"))    //获取值

	//遍历
	ma.Range(func(key, value interface{}) bool {
		fmt.Printf("key:%s ,value:%s \n", key, value)
		//如果返回:false,则退出循环,
		return true
	})
}
5、map映射过程:

哈希表中查找与某个键值对应的那个元素值,那么我们需要先把键值作为参数传给这个哈希表。哈希表会先用哈希函数(hash function)把键值转换为哈希值。哈希值通常是一个无符号的整数。一个哈希表会持有一定
数量的桶(bucket),也可称之为哈希桶,这些哈希桶会均匀地储存其所属哈希表收纳的那些键 - 元素对。因此,哈希表会先用这个键的哈希值的低几位去定位到一个哈希桶,然后再去这个哈希桶中,查找这个键。
由于键 - 元素对总是被捆绑在一起存储的,所以一旦找到了键,就一定能找到对应的元素值。随后,哈希表就会把相应的元素值作为结果返回。只要这个键 - 元素对存在于哈希表中就一定会被查找到。

下面是sync.Map源码:

type Mutex struct {
	state int32
	sema  uint32
}

type entry struct {
	p unsafe.Pointer // *interface{}
}

//sync.Map 包的结构
type Map struct {
   mu Mutex //锁
   
   /*
        由read字段的类型可知,sync.Map在替换只读字典的时候根本用不着锁。另外,这个只读字典
    在存储键值对的时候,还在值之上封装了一层。它先把值转换为了unsafe.Pointer类型的值,然后再把后者封装,并储存在其中的原生字典中。如此一来,在变更某个键所对应的值的时候,就也可以使用原子 *** 作了。
   */
   read atomic.Value// 只读字典
   /*
        它存储键值对的方式与read字段中的原生字典一致,它的键类型也是interface{},并且同样是把值先做   转换和封装后再进行储存的
   */
   dirty map[interface{}]*entry//脏字典。
   misses int//重建的判断条件
}

查找键值对的时候,会先去只读字典中寻找,并不需要锁定互斥锁。只有当确定“只读字典中没有,但脏字典中可能会有这个键”的时候,它才会在锁的保护下去访问脏字典。相对应的,sync.Map在存储键值对的时候,只要只读字典中已存有这个键,并且该键值对未被标记为“已删除”,就会把新值存到里面并直接返回,这种情况下也不需要用到锁。否则,它才会在锁的保护下把键值对存储到脏字典中。这个时候,该键值对的“已删除”标记会被
抹去。

当一个键值对应该被删除,但却仍然存在于只读字典中的时候,才会被用标记为“已删除”的方式进行逻辑删除,而不会直接被物理删除。

这种情况会在重建脏字典以后的一段时间内出现。不过,过不了多久,它们就会被真正删除掉。
在查找和遍历键值对的时候,已被逻辑删除的键值对永远会被无视。

对于删除键值对,sync.Map会先去检查只读字典中是否有对应的键。如果没有,脏字典中可能有,那么它就会在锁的保护下,试图从脏字典中删掉该键值对。最后,sync.Map会把该键值对中指向值的那个指针置为nil,这是另一种逻辑删除的方式。

除此之外,还有一个细节需要注意,只读字典和脏字典之间是会互相转换的。在脏字典中查找键
值对次数足够多的时候,sync.Map会把脏字典直接作为只读字典,保存在它的read字段中,然
后把代表脏字典的dirty字段的值置为nil。

在读 *** 作有很多但写 *** 作却很少的情况下,并发安全字典的性能往往会更好。在几个写 *** 作当中,新增键值对的 *** 作对并发安全字典的性能影响是最大的,其次是删除 *** 作,最后才是修改 *** 作。

如果被 *** 作的键值对已经存在于sync.Map的只读字典中,并且没有被逻辑删除,那么修改它并
不会使用到锁,对其性能的影响就会很小。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/996363.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存