18

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

 4 years ago
source link: https://studygolang.com/articles/26084
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Qv2A3eB.png!web

1. 定义

  • Go语言中映射是一种字典类型的数据结构,类似于c++和java中的hashmap,用于存储一系列无序的键值对。
  • 映射是基于键来存储值。映射的优势是能够基于键快速索引数据。键就像索引一样,指向与该键关联的值,在内存中键值对的关系如下图所示。

R7NRVfe.png!web

  • 和切片类似,映射维护的是底层数组的指针,属于引用类型。

2. 内部实现

  • 映射是一个集合,可以使用类似处理数组和切片的方式迭代映射中的元素。但 映射是无序的,不能对键值对进行排序。即使按顺序保存,每次迭代的时候顺序也不一样。 无序的原因是映射的实现使用了散列表。
  • 这个散列表是由hmap实现的,hmap中维护着存储键值对的bucket数组,hmap和bucket数组的关系如下图所示。

VjiYRnR.png!web

  • bucket是一个链表结构,每一个bucket后面连接的bucket表示bucket容量不够用时进行扩容添加新的bucket,bucket的数据结构如下图所示。

Mnuyaqu.png!web

  • 说到散列表,一定要有散列函数,散列函数是散列表中存取元素的关键。Go语言的映射中也有这样的散列函数(或叫哈希函数),但是Go语言把散列函数计算出的散列值可以说是用到了极致。它把散列值分成了高8位和低8位两部分,如下图所示。

a6VF7je.png!web

  • 散列值的作用

    • 低8位散列值: 用于寻找当前key位于哪个bucket ;
    • 高8位散列值: 用于寻找当前key位于bucket中的哪个位置
  • 映射的存取过程:在存储,删除或者查找键值对的时候,都要先将指定的键传给散列函数得到一个散列值,然后根据散列值的低8位选中对应的桶,最终再根据桶中的索引(散列值的高8位)将键值对分布到这个桶里。随着映射中存储的增加,索引分布越均匀,访问键值对的速度就越快。因此,映射通过设置合理数量的桶平衡键值对的分布。整个过程如下图所示。

ZBvaAvI.png!web

  • 键值对的存储方式:键值对的存储不是以key1,value1,key2,value2这样的形式存储, 主要是为了在key和value所占字节长度不同的时候,可以消除padding带来的空间浪费。
  • 映射的扩容:当散列表的容量需要增长的时候,Go语言会将bucket数组的容量扩充一倍,产生新的bucket数组,并将旧数据迁移到新数组。
  • 判断是否扩充的条件,就是散列表的加载因子,加载因子是一个阈值,表示散列表的空间利用率,Go语言map中的加载因子计算公式为:map长度/2^B,阈值是6.5,B表示已扩容的次数。
  • 映射中数据的删除

    • 如果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语言核心编程》第一章相关内容

mUv2iqf.jpg!web

我是lioney,年轻的后端攻城狮一枚,爱钻研,爱技术,爱分享。 个人笔记,整理不易,感谢阅读、点赞和收藏。 文章有任何问题欢迎大家指出,也欢迎大家一起交流后端各种问题!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK