go语言映射(map)要点总结

图片.png

1. 定义

  • Go语言中映射是一种字典类型的数据结构,相似于c++和java中的hashmap,用于存储一系列无序的键值对。
  • 映射是基于键来存储值。映射的优点是可以基于键快速索引数据。键就像索引同样,指向与该键关联的值,在内存中键值对的关系以下图所示。
图片.png
  • 和切片相似,映射维护的是底层数组的指针,属于引用类型。

2. 内部实现

  • 映射是一个集合,可使用相似处理数组和切片的方式迭代映射中的元素。但映射是无序的,不能对键值对进行排序。即便按顺序保存,每次迭代的时候顺序也不同。无序的缘由是映射的实现使用了散列表。
  • 这个散列表是由hmap实现的,hmap中维护着存储键值对的bucket数组,hmap和bucket数组的关系以下图所示。
图片.png
  • bucket是一个链表结构,每个bucket后面链接的bucket表示bucket容量不够用时进行扩容添加新的bucket,bucket的数据结构以下图所示。
图片.png
  • 说到散列表,必定要有散列函数,散列函数是散列表中存取元素的关键。Go语言的映射中也有这样的散列函数(或叫哈希函数),可是Go语言把散列函数计算出的散列值能够说是用到了极致。它把散列值分红了高8位和低8位两部分,以下图所示。
图片.png
  • 散列值的做用java

    • 低8位散列值:用于寻找当前key位于哪一个bucket;
    • 高8位散列值:用于寻找当前key位于bucket中的哪一个位置
  • 映射的存取过程:在存储,删除或者查找键值对的时候,都要先将指定的键传给散列函数获得一个散列值,而后根据散列值的低8位选中对应的桶,最终再根据桶中的索引(散列值的高8位)将键值对分布到这个桶里。随着映射中存储的增长,索引分布越均匀,访问键值对的速度就越快。所以,映射经过设置合理数量的桶平衡键值对的分布。整个过程以下图所示。
图片.png
  • 键值对的存储方式:键值对的存储不是以key1,value1,key2,value2这样的形式存储,主要是为了在key和value所占字节长度不一样的时候,能够消除padding带来的空间浪费。
  • 映射的扩容:当散列表的容量须要增加的时候,Go语言会将bucket数组的容量扩充一倍,产生新的bucket数组,并将旧数据迁移到新数组。
  • 判断是否扩充的条件,就是散列表的加载因子,加载因子是一个阈值,表示散列表的空间利用率,Go语言map中的加载因子计算公式为:map长度/2^B,阈值是6.5,B表示已扩容的次数。
  • 映射中数据的删除c++

    • 若是key是指针类型的,直接将其置空,等待GC清除;
    • 若是是值类型的,则清除相关内存;
    • 对value作一样的操做;
    • 把key对应的索引置为空。

3. 映射的建立

(1) 使用字面量建立

// 建立一个键为字符串类型,值为整形的map
    m1 := map[string]int{"last":2019, "now":2020}
    // 获取map中的元素
    fmt.Println(m1["last"])  // 2019
    fmt.Println(m1["now"])   // 2020
    
    // 使用字面量建立一个空map
    m2 := map[string]string{}
    fmt.Println(m2)  // map[]
映射的键的类型能够是内置类型,也能够是结构类型,但 这个类型必须可使用==运算符作比较。切片,函数以及包含切片的结构类型因为具备引用语义,不能做为映射的键,不然会形成编译错误。

(2) 使用内置的make函数来建立

m1 := make(map[int] string) // map的容量使用默认值
    m1[1] = "lioney"
    m2 := make(map[int] string, 10) // map的容量使用给定值
    m2[1] = "carlos"
    fmt.Println(m1[1])  // lioney
    fmt.Println(m2[1])  // carlos

4.映射支持的操做

  • map中单个键值的访问格式为mapName[key],能够用于获取或更新map中的元素
  • 可使用for range遍历map中的元素,不保证每次迭代顺序一致
  • 删除map中的某个键值对,使用语法delete(mapName, key)
  • 使用内置函数len()获取map中保存的键值对数量,map中不支持cap()函数
package main

    import "fmt"

    func main() {
        // 建立一个空的map
        m1 := make(map[int] string)
        // 向map中添加元素
        m1[1] = "lioney"
        m1[2] = "carlos"
        m1[3] = "tom"
        // 从map中删除键为3的元素
        delete(m1, 3)
        // len()表示map中键值对的数量
        fmt.Println("len=", len(m1))
        // 遍历map
        for k, v := range m1 {
            fmt.Println("key=", k, "value=", v)
        }
    }
上述代码编译后运行结果以下:
len= 2
    key= 2 value= carlos
    key= 1 value= lioney
    
    Process finished with exit code 0

5. 映射的使用要点

(1) 对nil映射赋值会产生运行时错误

和切片相似,映射在使用时必须对其底层数组进行初始化,要么使用make进行初始化,要么使用字面量初始化,若是只是简单地声明了一个map,而没有进行初始化,就是nil映射,是不能对其赋值的,请看下面代码:
// 声明一个map
    var colors map[string]string

    // 将red加入colors
    colors["red"] = "#da137"  // panic: assignment to entry in nil map
能够作以下修改:
// 声明一个map
    var colors map[string]string
    // 对map进行初始化
    colors = make(map[string]string)

    // 将red加入colors
    colors["red"] = "#da137" // no panic or error
也能够作以下修改:
// 使用字面量建立要给空map
    colors := map[string]string{}

    // 将red加入colors
    colors["red"] = "#da137"  // no panic or error
强烈推荐使用第二种,由于用字面量建立map比较简洁并且比较快

(2) 从映射获取值并判断键是否存在

在Go语言里,经过键来索引值时,即使这个键不存在也会返回一个值,有时候咱们须要判断获取到的值是否时默认的零值,代码以下所示。
// 使用字面量建立一个空map
    colors := map[string]string{}

    // 将red加入colors
    colors["red"] = "#da137" 
    
    // 获取blue对应的值并判断是否为零值
    value1, exists := colors["blue"]
    if exists {
        fmt.Println(value1)
    }
    
    // 也能够经过值直接判断是否为零值 
    value2 := colors["blue"]
    if value2 != "" {
        fmt.Println(value2)
    }

(3) 在函数间传递映射

package main

    import (
        "fmt"
        "unsafe"
    )

    func foo(m map[string]string) {
        // 打印参数的大小
        fmt.Println("参数大小", unsafe.Sizeof(m))
        // 删除键为green的元素
        delete(m, "green")
    }

    func main() {
        // 使用字面量建立一个空map
        colors := map[string]string{}

        // 将red加入colors
        colors["red"] = "#da137"
        colors["coral"] = "#ff7f50"
        colors["green"] = "#228b22"
        // 调用foo函数
        foo(colors)
        // 遍历打印map
        for k, v := range colors {
            fmt.Println("key=", k, "value=", v)
        }
    }
编译并运行以上代码,结果以下:
参数大小 8
    key= red value= #da137
    key= coral value= #ff7f50

    Process finished with exit code 0
映射是引用类型的数据结构,在函数间传递映射的时候,不会拷贝映射底层的数据,从上面代码的运行结果能够看出,只传递了一个8字节大小的指针, 实际上,map类型就是一个指针类型,函数对映射的操做都是经过这个指针间接进行的,所以对映射中数据的修改,其它引用到该映射的地方也能感知到

(4) 修改映射中的结构体类型,必须总体赋值(!!!)

package main

    import "fmt"

    type User struct {
        name string
        age  int
    }

    func main() {
        m := make(map[int]User)
        user := User{
            name: "lioney",
            age:  18,
        }
        // 将user加入到map中
        m[1] = user
        
        // 修改user
        // 不能经过map引用直接修改!!!
        //m[1].age = 2 // error: cannot assign to struct field m[1].age in map

        // 必须总体替换
        user.age = 2
        m[1] = user
        fmt.Println(m)
    }

(5) 并发问题

Go语言内置的map不是并发安全的,如果在并发场景下以共享的形式访问map中的元素,必须加锁,要想使用并发安全的map,可使用Go语言标准库中sync包下提供的map。

参考文献

  1. 博客https://blog.csdn.net/i644803...
  2. William Kennedy等《Go语言实战》第四章相关内容
  3. 李文塔《Go语言核心编程》第一章相关内容

我是lioney,年轻的后端攻城狮一枚,爱钻研,爱技术,爱分享。
我的笔记,整理不易,感谢阅读、点赞和收藏。
文章有任何问题欢迎你们指出,也欢迎你们一块儿交流后端各类问题!
相关文章
相关标签/搜索