Skip to content

map

Go 语言的 map 底层使用 Hash 表实现

热身测验

(1) 下面的代码存在什么问题?

1
2
3
4
5
var FruitColor map[string]string

func AddFruit(name, color string) {
    FruitColor[name] = color
}

解答:
未初始化的 map 变量默认值为 nil,向值为 nil 的 map 添加元素时会触发 panic。所以函数中应先判断 map 是否为 nil

(2) 下面的代码存在什么问题?

1
2
3
4
5
6
var StudentScore map[string]int

func GetScore(name string) int {
    score := StudentScore[name]
    return socre
}

解答:
查询 map 应判断元素是否存在,如果元素不存在则会返回值类型的零值,题目中的值类型为 int,当键不存在时,将返回 int 的零值(0)

特性速览

操作方式

(1) 初始化

map 分别支持字面量初始化和内置函数 make() 初始化

字面量初始化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func MapInitByLiteral() {
    m := map[string]int {
        "apple": 2,
        "banana": 3,
    }

    for k, v := range m {
        fmt.Printf("%s-%d\n", k, v)
    }
}

内置函数 make() 初始化:

1
2
3
4
5
6
7
8
9
func MapInitByMake() {
    m := make(map[string]int, 10)
    m["apple"] = 2
    m["banana"] = 3

    for k, v := range m {
        fmt.Printf("%s-%d\n", k, v)
    }
}

使用 make() 函数初始化时可以同时指定 map 的容量(也可以不指定)。指定容量可以有效地减少内存分配的次数,有利于提升应用性能。

(2) 增删改查

map 的增删改查操作比较简单,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func MapCRUD() {
    m := make(map[string]string, 10)
    m["apple"] = "red" // 添加
    m["apple"] = "green"  // 修改
    delete(m, "apple")  // 删除
    v, exist := m["apple"]  // 查询
    if exist {
        fmt.Printf("apple-%s\n", v)
    }
}

需要注意的是,在上面的修改操作中,如果键 "apple" 不存在,则 map 会创建一个新的键值对并存储,等同于添加新的元素。

删除元素使用内置函数 delete() 完成,delete() 没有返回值,在 map 为 nil 或指定的键不存在的情况下,delete() 也不会报错,相当于空操作。

在查询操作中,最多可以给两个变量赋值,第一个为值,第二个为 bool 类型的变量,用于指示是否存在指定的键,如果键不存在,那么第一个值为相应类型的零值。
如果只指定一个变量,那么该变量仅表示该键对应的值,如果键不存在,那么该值同样为相应类型的零值

内置函数 len() 可以查询 map 的长度,该长度反映 map 中存储的键值对数

危险操作

(1) 并发读写

map 操作并不是原子的,这意味着多个协程同时操作 map 时有可能产生读写冲突,读写冲突会触发 panic 从而导致程序退出

那么为什么 map 没有设计成支持并发读写呢?

这是因为 Go 团队在设计 map 时认为大多数场景下不需要并发读写,如果为了支持并发读写而引入互斥锁则会降低 map 的操作性能,得不偿失。
Go 是在 map 的实现中增加了读写检测机制,一旦发现读写冲突立即触发 panic,以免隐藏问题

(2) 空 map

未初始化的 map 的值为 nil,在向值为 nil 的 map 添加元素时会触发 panic,在使用时需要避免

值为 nil 的 map,长度与空 map 一致,如下所示:

1
2
3
4
5
6
7
func EmptyMap() {
    var m1 map[string]int
    m2 := make(map[string]int)

    fmt.Printf("len(m1) = %d\n", len(m1))  // 0
    fmt.Printf("len(m2) = %d\n", len(m2))  // 0
}

尽管操作值为 nil 的 map 没有意义,但查询、删除操作不会报错

小结

初始化 map 时推荐使用内置函数 make() 并指定预估的容量

修改键值对时,需要先查询指定键是否存在,否则 map 将创建新的键值对

查询键值对时,最好检查键是否存在,避免操作零值

避免并发读写 map,如果需要并发读写,则可以使用额外的锁(互斥锁、读写锁),也可以考虑使用标准库 sync 包中的 sync.Map