Go语言容器—Map

Map的概念

  • map 是引用类型,可以使用如下方式声明
    • var mapname map[keytype]valuetype
      • 提示:[keytype] 和 valuetype 之间允许有空格
    • 在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 pair 的数目

Map需要注意的一些点

  • map的创建方式之中,map[string]int{} 等价于 make(map[string]int)
  • := 是引用
  • 可以使用 make(),但不能使用 new() 来构造 map,如果错误的使用 new() 分配了一个引用对象,会获得一个空引用的指针,相当于声明了一个未初始化的变量并且取了它的地址
	var dataMap map[string]int
	var quoteMap map[string]int

	dataMap = map[string]int{"one": 1, "two": 2} //赋值

	createMap := make(map[string]int) //make创建个新的map,等价于mapCreated := map[string]int{} 

	quoteMap = dataMap //进行引用

	createMap["key1"] = 10
	createMap["key2"] = 20

	quoteMap["two"] = 999 //引用改值

	fmt.Println(dataMap)   //map[one:1 two:999]
	fmt.Println(createMap) //map[one:1 two:999]

Map的容量

  • 和数组不同,map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity,格式如下
    • make(map[keytype]valuetype, cap)
  • 当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明

用切片作为Map的值

  • 既然一个 key 只能对应一个 value,而 value 又是一个原始类型,那么如果一个 key 要对应多个值怎么办?例如,当我们要处理 unix 机器上的所有进程,以父进程(pid 为整形)作为 key,所有的子进程(以所有子进程的 pid 组成的切片)作为 value。通过将 value 定义为 []int 类型或者其他类型的切片,就可以优雅的解决这个问题,示例代码如下所示

    mp1 := make(map[int][]int)  //key:int value:切片,切片中元素为int
    mp2 := make(map[int]*[]int) //key:int value:切片,切片中元素为int*
    

Map的遍历

  • Go之中的map是无序的

  • map 的遍历过程使用 for range 循环完成

    var dataMap map[string]int
    	dataMap = map[string]int{"one": 1, "two": 2} //赋值
    	
    	for _, v := range dataMap {
    		fmt.Println(v) //2 1
    	}
    
    	//只需要键值时,无须将值改为匿名变量形式,忽略值即可。
    	for k := range dataMap {
    		fmt.Println(k) //one two
    	}
    

Map的删除和清空

  • 使用 delete() 函数从 map 中删除键值对

    	var dataMap map[string]int
    	dataMap = map[string]int{"one": 1, "two": 2} //赋值
    
    	delete(dataMap,"one")
    
    	for _, v := range dataMap {
    		fmt.Println(v) //2 
    	}
    
  • Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map,不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多

Map的多键索引

  • 在大多数的编程语言中,映射容器的键必须以单一值存在。这种映射方法经常被用在诸如信息检索上,如根据通讯簿的名字进行检索。但随着查询条件越来越复杂,检索也会变得越发困难。下面例子中涉及通讯簿的结构,结构如下

    //人员档案结构体
    type Profile struct {
    	Name    string // 名字
    	Age     int    // 年龄
    	Married bool   // 已婚
    }
    
    
    //初始化原始数据,并且实现构建索引和查询的过程
    func main() {
    	list := []*Profile{
    		{Name: "张三", Age: 30, Married: true},
    		{Name: "李四", Age: 21},
    		{Name: "王麻子", Age: 21},
    	}
    	buildIndex(list)
    	queryData("张三", 30)
    }
    
    • 基于哈希值的多键索引及查询

      // 查询键
      type classicQueryKey struct {
      	Name string // 要查询的名字
      	Age  int    // 要查询的年龄
      }
      
      //哈希函数
      //将传入的字符串的ASCII码相加起来然后返回
      func simpleHash(str string) (ret int) {
      	for i := 0; i < len(str); i++ {
      		c := str[i]
      		ret += int(c)
      	}
      	return ret
      }
      
      //计算查询键的哈希值,将查询键转换成对应的查询key
      func (c *classicQueryKey) hash() int {
      	return simpleHash(c.Name) + c.Age*1000000
      }
      
      // 创建哈希值到数据的索引关系
      //key:哈希值,value:对应的结构体指针切片
      var mapper = make(map[int][]*Profile)
      
      // 构建数据索引
      //遍历传入的切片之中的数据,构建对应的查询索引结构体,计算对应的hash值
      func buildIndex(list []*Profile) {
      	for _, profile := range list {
      		key := classicQueryKey{profile.Name, profile.Age} 
      		existValue := mapper[key.hash()]         //相同哈希值的所有结构体切片,先拿出来
      		existValue = append(existValue, profile) //将对应的结构体指针添加到切片之中
      		mapper[key.hash()] = existValue          //返回到对应的索引map之中
      	}
      }
      
      //查询函数
      //构建查询键,获取相同哈希值的所有结果
      //遍历得到的结果,并且与查询键进行对比
      func queryData(name string, age int) {
      	keyToQuery := classicQueryKey{name, age}
      	resultList := mapper[keyToQuery.hash()] //获取哈希对应的结构体指针切片
      
      	for _, result := range resultList {
      		if result.Name == name && result.Age == age { //进行对比
      			fmt.Println(result)
      			return
      		}
      	}
      	// 没有查询到时, 打印结果
      	fmt.Println("no found")
      }
      
    • 利用 map 特性的多键索引及查询

      • 使用结构体进行多键索引和查询比传统的写法更为简单,最主要的区别是无须准备哈希函数及相应的字段无须做哈希合并

        // 查询键
        type queryKey struct {
        	Name string
        	Age  int
        }
        
        // 创建查询键到数据的映射
        //key:对应的查询键结构体,value:对应的结构体指针
        var mapper = make(map[queryKey]*Profile)
        
        // 构建查询索引
        func buildIndex(list []*Profile) {
        	for _, profile := range list {
        		key := queryKey{
        			Name: profile.Name,
        			Age:  profile.Age,
        		}
        		mapper[key] = profile
        	}
        }
        
        //查询逻辑
        // 根据条件查询数据
        func queryData(name string, age int) {
        	key := queryKey{name, age}
        	result, ok := mapper[key]
        	if ok {
        		fmt.Println(result)
        	} else {
        		fmt.Println("no found")
        	}
        }
        
  • 代码量大大减少的关键是:Go语言的底层会为 map 的键自动构建哈希值。能够构建哈希值的类型必须是非动态类型、非指针、函数、闭包

    • 非动态类型:可用数组,不能用切片
    • 非指针:每个指针数值都不同,失去哈希意义
    • 数、闭包不能作为 map 的键

sync.Map

  • Go语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的

  • sync.Map特性

    • 无须初始化,直接声明即可

    • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除

    • 用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false

      var dataMap sync.Map
      
      	//将键值对存储至map之中
      	dataMap.Store("one",1)
      	dataMap.Store("two",2)
      	dataMap.Store("there",3)
      
      	//读取值
      	fmt.Println(dataMap.Load("there")) //3 true
      
      	//删除键值对
      	dataMap.Delete("one")
      
      	//遍历
      	dataMap.Range(func(k,v interface{} ) bool {
      		fmt.Println(k,v)  //two 2  there 3
      		return true
      	})
      
  • 需要注意点

    • sync.Map 不能使用 make 创建
    • sync.Map 将键和值以 interface{} 类型进行保存
    • sync.Map 的 Delete 可以使用指定的键将对应的键值对删除
    • Range() 方法可以遍历 sync.Map,遍历需要提供一个匿名函数,参数为 k、v,类型为 interface{},每次 Range() 在遍历一个元素时,都会调用这个匿名函数把结果返回
    • sync.Map 没有提供获取 map 数量的方法,替代方法是在获取 sync.Map 时遍历自行计算数量,sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值